vastly simplify and cleanup collection initialization by internally

introducing a RootCollector. Note that the internal node
methods _fromtrail and _totrail are shifted to the still internal
config._rootcol.fromtrail/totrail

--HG--
branch : trunk
This commit is contained in:
holger krekel 2010-01-03 01:02:44 +01:00
parent eebeb1b257
commit 1b34492108
9 changed files with 127 additions and 165 deletions

View File

@ -55,6 +55,7 @@ Changes between 1.X and 1.1.1
which will regularly see e.g. py.test.mark and py.test.importorskip. which will regularly see e.g. py.test.mark and py.test.importorskip.
- simplify internal plugin manager machinery - simplify internal plugin manager machinery
- simplify internal collection tree by introducing a RootCollector node
- fix assert reinterpreation that sees a call containing "keyword=..." - fix assert reinterpreation that sees a call containing "keyword=..."

View File

@ -115,7 +115,7 @@ class Node(object):
l = [self] l = [self]
while 1: while 1:
x = l[-1] x = l[-1]
if x.parent is not None: if x.parent is not None and x.parent.parent is not None:
l.append(x.parent) l.append(x.parent)
else: else:
if not rootfirst: if not rootfirst:
@ -130,62 +130,7 @@ class Node(object):
while current and not isinstance(current, cls): while current and not isinstance(current, cls):
current = current.parent current = current.parent
return current return current
def _getitembynames(self, namelist):
cur = self
for name in namelist:
if name:
next = cur.collect_by_name(name)
if next is None:
existingnames = [x.name for x in self._memocollect()]
msg = ("Collector %r does not have name %r "
"existing names are: %s" %
(cur, name, existingnames))
raise AssertionError(msg)
cur = next
return cur
def _getfsnode(self, path):
# this method is usually called from
# config.getfsnode() which returns a colitem
# from filename arguments
#
# pytest's collector tree does not neccessarily
# follow the filesystem and we thus need to do
# some special matching code here because
# _getitembynames() works by colitem names, not
# basenames.
if path == self.fspath:
return self
basenames = path.relto(self.fspath).split(path.sep)
cur = self
while basenames:
basename = basenames.pop(0)
assert basename
fspath = cur.fspath.join(basename)
colitems = cur._memocollect()
l = []
for colitem in colitems:
if colitem.fspath == fspath or colitem.name == basename:
l.append(colitem)
if not l:
raise self.config.Error("can't collect: %s" %(fspath,))
if basenames:
if len(l) > 1:
msg = ("Collector %r has more than one %r colitem "
"existing colitems are: %s" %
(cur, fspath, colitems))
raise self.config.Error("xxx-too many test types for: %s" % (fspath, ))
cur = l[0]
else:
if len(l) > 1:
cur = l
else:
cur = l[0]
break
return cur
def readkeywords(self): def readkeywords(self):
return dict([(x, True) for x in self._keywords()]) return dict([(x, True) for x in self._keywords()])
@ -228,30 +173,6 @@ class Node(object):
def _prunetraceback(self, traceback): def _prunetraceback(self, traceback):
return 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): def _repr_failure_py(self, excinfo):
excinfo.traceback = self._prunetraceback(excinfo.traceback) excinfo.traceback = self._prunetraceback(excinfo.traceback)
# XXX should excinfo.getrepr record all data and toterminal() # XXX should excinfo.getrepr record all data and toterminal()
@ -347,30 +268,15 @@ class FSCollector(Collector):
self.fspath = fspath self.fspath = fspath
def __getstate__(self): def __getstate__(self):
if self.parent is None: if isinstance(self.parent, RootCollector):
# the root node needs to pickle more context info relpath = self.parent._getrelpath(self.fspath)
topdir = self.config.topdir return (relpath, self.parent)
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: else:
return (self.name, self.parent) return (self.name, self.parent)
def __setstate__(self, picklestate): def __setstate__(self, picklestate):
if len(picklestate) == 3: name, parent = picklestate
# root node self.__init__(parent.fspath.join(name), parent=parent)
name, config, relpath = picklestate
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 File(FSCollector): class File(FSCollector):
""" base class for collecting tests from a file. """ """ base class for collecting tests from a file. """
@ -421,7 +327,8 @@ class Directory(FSCollector):
l = [] l = []
for x in res: for x in res:
if x not in l: if x not in l:
assert x.parent == self, "wrong collection tree construction" assert x.parent == self, (x.parent, self)
assert x.fspath == path, (x.fspath, path)
l.append(x) l.append(x)
res = l res = l
return res return res
@ -468,3 +375,67 @@ def warnoldtestrun(function=None):
"implement item.runtest() instead of " "implement item.runtest() instead of "
"item.run() and item.execute()", "item.run() and item.execute()",
stacklevel=2, function=function) stacklevel=2, function=function)
class RootCollector(Directory):
def __init__(self, config):
Directory.__init__(self, config.topdir, parent=None, config=config)
self.name = None
def getfsnode(self, path):
path = py.path.local(path)
if not path.check():
raise self.config.Error("file not found: %s" %(path,))
topdir = self.config.topdir
if path != topdir and not path.relto(topdir):
raise self.config.Error("path %r is not relative to %r" %
(str(path), str(self.fspath)))
# assumtion: pytest's fs-collector tree follows the filesystem tree
basenames = filter(None, path.relto(topdir).split(path.sep))
try:
return self.getbynames(basenames)
except ValueError:
raise self.config.Error("can't collect: %s" % str(path))
def getbynames(self, names):
current = self.consider(self.config.topdir)
for name in names:
if name == ".": # special "identity" name
continue
l = []
for x in current._memocollect():
if x.name == name:
l.append(x)
elif x.fspath == current.fspath.join(name):
l.append(x)
if not l:
raise ValueError("no node named %r in %r" %(name, current))
current = l[0]
return current
def totrail(self, node):
chain = node.listchain()
names = [self._getrelpath(chain[0].fspath)]
names += [x.name for x in chain[1:]]
return names
def fromtrail(self, trail):
return self.config._rootcol.getbynames(trail)
def _getrelpath(self, fspath):
topdir = self.config.topdir
relpath = fspath.relto(topdir)
if not relpath:
if fspath == topdir:
relpath = "."
else:
raise ValueError("%r not relative to topdir %s"
%(self.fspath, topdir))
return relpath
def __getstate__(self):
return self.config
def __setstate__(self, config):
self.__init__(config)

View File

@ -2,6 +2,7 @@ import py, os
from py.impl.test.conftesthandle import Conftest from py.impl.test.conftesthandle import Conftest
from py.impl.test.pluginmanager import PluginManager from py.impl.test.pluginmanager import PluginManager
from py.impl.test import parseopt from py.impl.test import parseopt
from py.impl.test.collect import RootCollector
def ensuretemp(string, dir=1): def ensuretemp(string, dir=1):
""" (deprecated) return temporary directory path with """ (deprecated) return temporary directory path with
@ -97,6 +98,7 @@ class Config(object):
if not args: if not args:
args.append(py.std.os.getcwd()) args.append(py.std.os.getcwd())
self.topdir = gettopdir(args) self.topdir = gettopdir(args)
self._rootcol = RootCollector(config=self)
self.args = [py.path.local(x) for x in args] self.args = [py.path.local(x) for x in args]
# config objects are usually pickled across system # config objects are usually pickled across system
@ -117,6 +119,7 @@ class Config(object):
py.test.config = self py.test.config = self
# next line will registers default plugins # next line will registers default plugins
self.__init__(topdir=py.path.local()) self.__init__(topdir=py.path.local())
self._rootcol = RootCollector(config=self)
args, cmdlineopts = repr args, cmdlineopts = repr
args = [self.topdir.join(x) for x in args] args = [self.topdir.join(x) for x in args]
self.option = cmdlineopts self.option = cmdlineopts
@ -150,17 +153,7 @@ class Config(object):
return [self.getfsnode(arg) for arg in self.args] return [self.getfsnode(arg) for arg in self.args]
def getfsnode(self, path): def getfsnode(self, path):
path = py.path.local(path) return self._rootcol.getfsnode(path)
if not path.check():
raise self.Error("file not found: %s" %(path,))
# we want our possibly custom collection tree to start at pkgroot
pkgpath = path.pypkgpath()
if pkgpath is None:
pkgpath = path.check(file=1) and path.dirpath() or path
tmpcol = py.test.collect.Directory(pkgpath, config=self)
col = tmpcol.ihook.pytest_collect_directory(path=pkgpath, parent=tmpcol)
col.parent = None
return col._getfsnode(path)
def _getcollectclass(self, name, path): def _getcollectclass(self, name, path):
try: try:

View File

@ -136,8 +136,8 @@ def slave_runsession(channel, config, fullwidth, hasmarkup):
colitems = [] colitems = []
for trail in trails: for trail in trails:
try: try:
colitem = py.test.collect.Collector._fromtrail(trail, config) colitem = config._rootcol.fromtrail(trail)
except AssertionError: except ValueError:
#XXX send info for "test disappeared" or so #XXX send info for "test disappeared" or so
continue continue
colitems.append(colitem) colitems.append(colitem)
@ -159,4 +159,5 @@ def slave_runsession(channel, config, fullwidth, hasmarkup):
session.config.hook.pytest_looponfailinfo( session.config.hook.pytest_looponfailinfo(
failreports=list(failreports), failreports=list(failreports),
rootdirs=[config.topdir]) rootdirs=[config.topdir])
channel.send([rep.getnode()._totrail() for rep in failreports]) rootcol = session.config._rootcol
channel.send([rootcol.totrail(rep.getnode()) for rep in failreports])

View File

@ -4,9 +4,9 @@ from py.plugin.pytest_resultlog import generic_path, ResultLog
from py.impl.test.collect import Node, Item, FSCollector from py.impl.test.collect import Node, Item, FSCollector
def test_generic_path(testdir): def test_generic_path(testdir):
config = testdir.Config() config = testdir.parseconfig()
p1 = Node('a', config=config) p1 = Node('a', parent=config._rootcol)
assert p1.fspath is None #assert p1.fspath is None
p2 = Node('B', parent=p1) p2 = Node('B', parent=p1)
p3 = Node('()', parent = p2) p3 = Node('()', parent = p2)
item = Item('c', parent = p3) item = Item('c', parent = p3)
@ -14,7 +14,7 @@ def test_generic_path(testdir):
res = generic_path(item) res = generic_path(item)
assert res == 'a.B().c' assert res == 'a.B().c'
p0 = FSCollector('proj/test', config=config) p0 = FSCollector('proj/test', parent=config._rootcol)
p1 = FSCollector('proj/test/a', parent=p0) p1 = FSCollector('proj/test/a', parent=p0)
p2 = Node('B', parent=p1) p2 = Node('B', parent=p1)
p3 = Node('()', parent = p2) p3 = Node('()', parent = p2)

View File

@ -52,44 +52,8 @@ class TestCollector:
parent = fn.getparent(py.test.collect.Class) parent = fn.getparent(py.test.collect.Class)
assert parent is cls assert parent is cls
def test_totrail_and_back(self, testdir, tmpdir):
a = tmpdir.ensure("a", dir=1)
tmpdir.ensure("a", "__init__.py")
x = tmpdir.ensure("a", "trail.py")
config = testdir.reparseconfig([x])
col = config.getfsnode(x)
trail = col._totrail()
assert len(trail) == 2
assert trail[0] == a.relto(config.topdir)
assert trail[1] == ('trail.py',)
col2 = py.test.collect.Collector._fromtrail(trail, config)
assert col2.listnames() == col.listnames()
def test_totrail_topdir_and_beyond(self, testdir, tmpdir):
config = testdir.reparseconfig()
col = config.getfsnode(config.topdir)
trail = col._totrail()
assert len(trail) == 2
assert trail[0] == '.'
assert trail[1] == ()
col2 = py.test.collect.Collector._fromtrail(trail, config)
assert col2.fspath == config.topdir
assert len(col2.listchain()) == 1
col3 = config.getfsnode(config.topdir.dirpath())
py.test.raises(ValueError,
"col3._totrail()")
def test_listnames_and__getitembynames(self, testdir): def test_getcustomfile_roundtrip(self, testdir):
modcol = testdir.getmodulecol("pass", withinit=True)
print(modcol.config.pluginmanager.getplugins())
names = modcol.listnames()
print(names)
dircol = modcol.config.getfsnode(modcol.config.topdir)
x = dircol._getitembynames(names)
assert modcol.name == x.name
def test_listnames_getitembynames_custom(self, testdir):
hello = testdir.makefile(".xxx", hello="world") hello = testdir.makefile(".xxx", hello="world")
testdir.makepyfile(conftest=""" testdir.makepyfile(conftest="""
import py import py
@ -98,15 +62,15 @@ class TestCollector:
class MyDirectory(py.test.collect.Directory): class MyDirectory(py.test.collect.Directory):
def collect(self): def collect(self):
return [CustomFile(self.fspath.join("hello.xxx"), parent=self)] return [CustomFile(self.fspath.join("hello.xxx"), parent=self)]
Directory = MyDirectory def pytest_collect_directory(path, parent):
return MyDirectory(path, parent=parent)
""") """)
config = testdir.parseconfig(hello) config = testdir.parseconfig(hello)
node = config.getfsnode(hello) node = config.getfsnode(hello)
assert isinstance(node, py.test.collect.File) assert isinstance(node, py.test.collect.File)
assert node.name == "hello.xxx" assert node.name == "hello.xxx"
names = node.listnames()[1:] names = config._rootcol.totrail(node)
dircol = config.getfsnode(config.topdir) node = config._rootcol.getbynames(names)
node = dircol._getitembynames(names)
assert isinstance(node, py.test.collect.File) assert isinstance(node, py.test.collect.File)
class TestCollectFS: class TestCollectFS:
@ -232,3 +196,27 @@ class TestCustomConftests:
"*MyModule*", "*MyModule*",
"*test_x*" "*test_x*"
]) ])
class TestRootCol:
def test_totrail_and_back(self, testdir, tmpdir):
a = tmpdir.ensure("a", dir=1)
tmpdir.ensure("a", "__init__.py")
x = tmpdir.ensure("a", "trail.py")
config = testdir.reparseconfig([x])
col = config.getfsnode(x)
trail = config._rootcol.totrail(col)
col2 = config._rootcol.fromtrail(trail)
assert col2 == col
def test_totrail_topdir_and_beyond(self, testdir, tmpdir):
config = testdir.reparseconfig()
col = config.getfsnode(config.topdir)
trail = config._rootcol.totrail(col)
col2 = config._rootcol.fromtrail(trail)
assert col2.fspath == config.topdir
assert len(col2.listchain()) == 1
py.test.raises(config.Error, "config.getfsnode(config.topdir.dirpath())")
#col3 = config.getfsnode(config.topdir.dirpath())
#py.test.raises(ValueError,
# "col3._totrail()")

View File

@ -1,4 +1,5 @@
import py import py
from py.impl.test.collect import RootCollector
class TestConfigCmdlineParsing: class TestConfigCmdlineParsing:
@ -153,7 +154,7 @@ class TestConfigApi_getcolitems:
assert isinstance(col, py.test.collect.Module) assert isinstance(col, py.test.collect.Module)
assert col.name == 'x.py' assert col.name == 'x.py'
assert col.parent.name == tmpdir.basename assert col.parent.name == tmpdir.basename
assert col.parent.parent is None assert isinstance(col.parent.parent, RootCollector)
for col in col.listchain(): for col in col.listchain():
assert col.config is config assert col.config is config
@ -164,7 +165,7 @@ class TestConfigApi_getcolitems:
assert isinstance(col, py.test.collect.Directory) assert isinstance(col, py.test.collect.Directory)
print(col.listchain()) print(col.listchain())
assert col.name == 'a' assert col.name == 'a'
assert col.parent is None assert isinstance(col.parent, RootCollector)
assert col.config is config assert col.config is config
def test__getcol_pkgfile(self, testdir, tmpdir): def test__getcol_pkgfile(self, testdir, tmpdir):
@ -175,7 +176,7 @@ class TestConfigApi_getcolitems:
assert isinstance(col, py.test.collect.Module) assert isinstance(col, py.test.collect.Module)
assert col.name == 'x.py' assert col.name == 'x.py'
assert col.parent.name == x.dirpath().basename assert col.parent.name == x.dirpath().basename
assert col.parent.parent is None assert isinstance(col.parent.parent.parent, RootCollector)
for col in col.listchain(): for col in col.listchain():
assert col.config is config assert col.config is config

View File

@ -5,7 +5,7 @@ from py.impl.test.outcome import Skipped
class TestCollectDeprecated: class TestCollectDeprecated:
def test_collect_with_deprecated_run_and_join(self, testdir, recwarn): def test_collect_with_deprecated_run_and_join(self, testdir, recwarn):
testdir.makepyfile(conftest=""" testdir.makeconftest("""
import py import py
class MyInstance(py.test.collect.Instance): class MyInstance(py.test.collect.Instance):
@ -39,7 +39,8 @@ class TestCollectDeprecated:
return self.Module(self.fspath.join(name), parent=self) return self.Module(self.fspath.join(name), parent=self)
def pytest_collect_directory(path, parent): def pytest_collect_directory(path, parent):
return MyDirectory(path, parent) if path.basename == "subconf":
return MyDirectory(path, parent)
""") """)
subconf = testdir.mkpydir("subconf") subconf = testdir.mkpydir("subconf")
somefile = subconf.join("somefile.py") somefile = subconf.join("somefile.py")

View File

@ -434,3 +434,9 @@ def test_generate_tests_only_done_in_subdir(testdir):
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*3 passed*" "*3 passed*"
]) ])
def test_modulecol_roundtrip(testdir):
modcol = testdir.getmodulecol("pass", withinit=True)
trail = modcol.config._rootcol.totrail(modcol)
newcol = modcol.config._rootcol.fromtrail(trail)
assert modcol.name == newcol.name