generalize hook calling from collection nodes but stop short

of allowing general hooks in python test modules. It'd be
easily possible (a 1-line change) but considering it i refrained
from it because the collector API is a bit too low level.

pytest_generate_tests and funcarg factories have a limited
directly useful interface and are thus less confusing - those
are taking advantage of hook discovery in python test modules.

--HG--
branch : trunk
This commit is contained in:
holger krekel 2009-12-30 10:42:01 +01:00
parent 631dfe9f13
commit ae63605ac0
7 changed files with 65 additions and 8 deletions

View File

@ -9,6 +9,8 @@ Changes between 1.X and 1.1.1
- new "pytestconfig" funcarg allows access to test config object - new "pytestconfig" funcarg allows access to test config object
- allow pytest_generate_tests to be defined in classes as well
- collection/item node specific runtest/collect hooks are only called exactly - collection/item node specific runtest/collect hooks are only called exactly
on matching conftest.py files, i.e. ones which are exactly below on matching conftest.py files, i.e. ones which are exactly below
the filesystem path of an item the filesystem path of an item

View File

@ -19,7 +19,7 @@ class HookProxy:
raise AttributeError(name) raise AttributeError(name)
hookmethod = getattr(self.node.config.hook, name) hookmethod = getattr(self.node.config.hook, name)
def call_matching_hooks(**kwargs): def call_matching_hooks(**kwargs):
plugins = self.node.config.getmatchingplugins(self.node.fspath) plugins = self.node._getplugins()
return hookmethod.pcall(plugins, **kwargs) return hookmethod.pcall(plugins, **kwargs)
return call_matching_hooks return call_matching_hooks
@ -43,6 +43,9 @@ class Node(object):
self.fspath = getattr(parent, 'fspath', None) self.fspath = getattr(parent, 'fspath', None)
self.ihook = HookProxy(self) self.ihook = HookProxy(self)
def _getplugins(self):
return self.config._getmatchingplugins(self.fspath)
def _checkcollectable(self): def _checkcollectable(self):
if not hasattr(self, 'fspath'): if not hasattr(self, 'fspath'):
self.parent._memocollect() # to reraise exception self.parent._memocollect() # to reraise exception

View File

@ -45,7 +45,7 @@ class Config(object):
self.trace("loaded conftestmodule %r" %(conftestmodule,)) self.trace("loaded conftestmodule %r" %(conftestmodule,))
self.pluginmanager.consider_conftest(conftestmodule) self.pluginmanager.consider_conftest(conftestmodule)
def getmatchingplugins(self, fspath): def _getmatchingplugins(self, fspath):
conftests = self._conftest._conftestpath2mod.values() conftests = self._conftest._conftestpath2mod.values()
plugins = [x for x in self.pluginmanager.getplugins() plugins = [x for x in self.pluginmanager.getplugins()
if x not in conftests] if x not in conftests]

View File

@ -93,10 +93,7 @@ class FuncargRequest:
self.fspath = pyfuncitem.fspath self.fspath = pyfuncitem.fspath
if hasattr(pyfuncitem, '_requestparam'): if hasattr(pyfuncitem, '_requestparam'):
self.param = pyfuncitem._requestparam self.param = pyfuncitem._requestparam
self._plugins = self.config.getmatchingplugins(self.fspath) self._plugins = pyfuncitem._getplugins(withpy=True)
self._plugins.append(self.module)
if self.instance is not None:
self._plugins.append(self.instance)
self._funcargs = self._pyfuncitem.funcargs.copy() self._funcargs = self._pyfuncitem.funcargs.copy()
self._name2factory = {} self._name2factory = {}
self._currentarg = None self._currentarg = None

View File

@ -34,6 +34,15 @@ class PyobjMixin(object):
return property(fget, fset, None, "underlying python object") return property(fget, fset, None, "underlying python object")
obj = obj() obj = obj()
def _getplugins(self, withpy=False):
plugins = self.config._getmatchingplugins(self.fspath)
if withpy:
plugins.append(self.getparent(py.test.collect.Module).obj)
inst = self.getparent(py.test.collect.Instance)
if inst is not None:
plugins.append(inst.obj)
return plugins
def _getobj(self): def _getobj(self):
return getattr(self.parent.obj, self.name) return getattr(self.parent.obj, self.name)
@ -138,8 +147,7 @@ class PyCollectorMixin(PyobjMixin, py.test.collect.Collector):
metafunc = funcargs.Metafunc(funcobj, config=self.config, metafunc = funcargs.Metafunc(funcobj, config=self.config,
cls=cls, module=module) cls=cls, module=module)
gentesthook = self.config.hook.pytest_generate_tests gentesthook = self.config.hook.pytest_generate_tests
plugins = self.config.getmatchingplugins(self.fspath) + [module] gentesthook.pcall(self._getplugins(withpy=True), metafunc=metafunc)
gentesthook.pcall(plugins, metafunc=metafunc)
if not metafunc._calls: if not metafunc._calls:
return self.Function(name, parent=self) return self.Function(name, parent=self)
return funcargs.FunctionCollector(name=name, return funcargs.FunctionCollector(name=name,
@ -327,6 +335,7 @@ class Function(FunctionMixin, py.test.collect.Item):
self.funcargs = {} self.funcargs = {}
if callobj is not _dummy: if callobj is not _dummy:
self._obj = callobj self._obj = callobj
self.function = getattr(self.obj, 'im_func', self.obj)
def _isyieldedfunction(self): def _isyieldedfunction(self):
return self._args is not None return self._args is not None

View File

@ -285,3 +285,34 @@ def test_callinfo():
assert not hasattr(ci, 'result') assert not hasattr(ci, 'result')
assert ci.excinfo assert ci.excinfo
assert "exc" in repr(ci) assert "exc" in repr(ci)
# design question: do we want general hooks in python files?
# following passes if withpy defaults to True in pycoll.PyObjMix._getplugins()
@py.test.mark.xfail
def test_runtest_in_module_ordering(testdir):
p1 = testdir.makepyfile("""
def pytest_runtest_setup(item): # runs after class-level!
item.function.mylist.append("module")
class TestClass:
def pytest_runtest_setup(self, item):
assert not hasattr(item.function, 'mylist')
item.function.mylist = ['class']
def pytest_funcarg__mylist(self, request):
return request.function.mylist
def pytest_runtest_call(self, item, __multicall__):
try:
__multicall__.execute()
except ValueError:
pass
def test_hello1(self, mylist):
assert mylist == ['class', 'module'], mylist
raise ValueError()
def test_hello2(self, mylist):
assert mylist == ['class', 'module'], mylist
def pytest_runtest_teardown(item):
del item.function.mylist
""")
result = testdir.runpytest(p1)
assert result.stdout.fnmatch_lines([
"*2 passed*"
])

View File

@ -483,6 +483,21 @@ class TestGenfuncFunctional:
"*1 failed, 1 passed*" "*1 failed, 1 passed*"
]) ])
def test_generate_tests_in_class(self, testdir):
p = testdir.makepyfile("""
class TestClass:
def pytest_generate_tests(self, metafunc):
metafunc.addcall(funcargs={'hello': 'world'}, id="hello")
def test_myfunc(self, hello):
assert hello == "world"
""")
result = testdir.runpytest("-v", p)
assert result.stdout.fnmatch_lines([
"*test_myfunc*hello*PASS*",
"*1 passed*"
])
def test_conftest_funcargs_only_available_in_subdir(testdir): def test_conftest_funcargs_only_available_in_subdir(testdir):
sub1 = testdir.mkpydir("sub1") sub1 = testdir.mkpydir("sub1")