refactor internal finalization mechanics such that all fixture arguments
in a test invocation will have a corresponding FixtureDef instance. also fixes issue246 (again). simplify parametrized fixture teardown by making it work lazy: during the setup of a parametrized fixture instance any previously setup instance which was setup with a different param is torn down before setting up the new one.
This commit is contained in:
parent
4b9dbd3920
commit
4f0879ff9b
13
CHANGELOG
13
CHANGELOG
|
@ -1,6 +1,19 @@
|
||||||
Unreleased
|
Unreleased
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
- simplified and fixed implementation for calling finalizers when
|
||||||
|
parametrized fixtures or function arguments are involved. finalization
|
||||||
|
is now performed lazily at setup time instead of in the "teardown phase".
|
||||||
|
While this might sound odd at first, it helps to ensure that we are
|
||||||
|
correctly handling setup/teardown even in complex code. User-level code
|
||||||
|
should not be affected unless it's implementing the pytest_runtest_teardown
|
||||||
|
hook and expecting certain fixture instances are torn down within (very
|
||||||
|
unlikely and would have been unreliable anyway).
|
||||||
|
|
||||||
|
- fix issue246 (again) fix finalizer order to be LIFO on independent fixtures
|
||||||
|
depending on a parametrized higher-than-function scoped fixture.
|
||||||
|
(fix quite some effort so please bear with the complexity of this sentence :)
|
||||||
|
|
||||||
- fix issue244 by implementing special index for parameters to only use
|
- fix issue244 by implementing special index for parameters to only use
|
||||||
indices for paramentrized test ids
|
indices for paramentrized test ids
|
||||||
|
|
||||||
|
|
|
@ -230,6 +230,8 @@ class Node(object):
|
||||||
#: allow adding of extra keywords to use for matching
|
#: allow adding of extra keywords to use for matching
|
||||||
self.extra_keyword_matches = set()
|
self.extra_keyword_matches = set()
|
||||||
|
|
||||||
|
# used for storing artificial fixturedefs for direct parametrization
|
||||||
|
self._name2pseudofixturedef = {}
|
||||||
#self.extrainit()
|
#self.extrainit()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -365,6 +367,8 @@ class Node(object):
|
||||||
self.session._setupstate.addfinalizer(fin, self)
|
self.session._setupstate.addfinalizer(fin, self)
|
||||||
|
|
||||||
def getparent(self, cls):
|
def getparent(self, cls):
|
||||||
|
""" get the next parent node (including ourself)
|
||||||
|
which is an instance of the given class"""
|
||||||
current = self
|
current = self
|
||||||
while current and not isinstance(current, cls):
|
while current and not isinstance(current, cls):
|
||||||
current = current.parent
|
current = current.parent
|
||||||
|
|
|
@ -341,12 +341,72 @@ class PyCollector(PyobjMixin, pytest.Collector):
|
||||||
if not metafunc._calls:
|
if not metafunc._calls:
|
||||||
yield Function(name, parent=self)
|
yield Function(name, parent=self)
|
||||||
else:
|
else:
|
||||||
|
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
|
||||||
|
add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
||||||
|
|
||||||
for callspec in metafunc._calls:
|
for callspec in metafunc._calls:
|
||||||
subname = "%s[%s]" %(name, callspec.id)
|
subname = "%s[%s]" %(name, callspec.id)
|
||||||
yield Function(name=subname, parent=self,
|
yield Function(name=subname, parent=self,
|
||||||
callspec=callspec, callobj=funcobj,
|
callspec=callspec, callobj=funcobj,
|
||||||
keywords={callspec.id:True})
|
keywords={callspec.id:True})
|
||||||
|
|
||||||
|
def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager):
|
||||||
|
# this function will transform all collected calls to a functions
|
||||||
|
# if they use direct funcargs (i.e. direct parametrization)
|
||||||
|
# because we want later test execution to be able to rely on
|
||||||
|
# an existing FixtureDef structure for all arguments.
|
||||||
|
# XXX we can probably avoid this algorithm if we modify CallSpec2
|
||||||
|
# to directly care for creating the fixturedefs within its methods.
|
||||||
|
if not metafunc._calls[0].funcargs:
|
||||||
|
return # this function call does not have direct parametrization
|
||||||
|
# collect funcargs of all callspecs into a list of values
|
||||||
|
arg2params = {}
|
||||||
|
arg2scope = {}
|
||||||
|
arg2fixturedefs = metafunc._arg2fixturedefs
|
||||||
|
for param_index, callspec in enumerate(metafunc._calls):
|
||||||
|
for argname, argvalue in callspec.funcargs.items():
|
||||||
|
assert argname not in arg2fixturedefs
|
||||||
|
arg2params.setdefault(argname, []).append(argvalue)
|
||||||
|
if argname not in arg2scope:
|
||||||
|
scopenum = callspec._arg2scopenum.get(argname, scopenum_function)
|
||||||
|
arg2scope[argname] = scopes[scopenum]
|
||||||
|
callspec.indices[argname] = param_index
|
||||||
|
|
||||||
|
for argname in callspec.funcargs:
|
||||||
|
assert argname not in callspec.params
|
||||||
|
callspec.params.update(callspec.funcargs)
|
||||||
|
callspec.funcargs.clear()
|
||||||
|
|
||||||
|
# register artificial FixtureDef's so that later at test execution
|
||||||
|
# time we can rely on a proper FixtureDef to exist for fixture setup.
|
||||||
|
for argname, valuelist in arg2params.items():
|
||||||
|
# if we have a scope that is higher than function we need
|
||||||
|
# to make sure we only ever create an according fixturedef on
|
||||||
|
# a per-scope basis. We thus store and cache the fixturedef on the
|
||||||
|
# node related to the scope.
|
||||||
|
assert argname not in arg2fixturedefs, (argname, arg2fixturedefs)
|
||||||
|
scope = arg2scope[argname]
|
||||||
|
node = None
|
||||||
|
if scope != "function":
|
||||||
|
node = get_scope_node(collector, scope)
|
||||||
|
if node is None:
|
||||||
|
assert scope == "class" and isinstance(collector, Module)
|
||||||
|
# use module-level collector for class-scope (for now)
|
||||||
|
node = collector
|
||||||
|
if node and argname in node._name2pseudofixturedef:
|
||||||
|
arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]]
|
||||||
|
else:
|
||||||
|
fixturedef = FixtureDef(fixturemanager, '', argname,
|
||||||
|
get_direct_param_fixture_func,
|
||||||
|
arg2scope[argname],
|
||||||
|
valuelist, False, False)
|
||||||
|
arg2fixturedefs[argname] = [fixturedef]
|
||||||
|
if node is not None:
|
||||||
|
node._name2pseudofixturedef[argname] = fixturedef
|
||||||
|
|
||||||
|
|
||||||
|
def get_direct_param_fixture_func(request):
|
||||||
|
return request.param
|
||||||
|
|
||||||
class FuncFixtureInfo:
|
class FuncFixtureInfo:
|
||||||
def __init__(self, argnames, names_closure, name2fixturedefs):
|
def __init__(self, argnames, names_closure, name2fixturedefs):
|
||||||
|
@ -560,25 +620,24 @@ def hasinit(obj):
|
||||||
|
|
||||||
def fillfixtures(function):
|
def fillfixtures(function):
|
||||||
""" fill missing funcargs for a test function. """
|
""" fill missing funcargs for a test function. """
|
||||||
if 1 or getattr(function, "_args", None) is None: # not a yielded function
|
try:
|
||||||
try:
|
request = function._request
|
||||||
request = function._request
|
except AttributeError:
|
||||||
except AttributeError:
|
# XXX this special code path is only expected to execute
|
||||||
# XXX this special code path is only expected to execute
|
# with the oejskit plugin. It uses classes with funcargs
|
||||||
# with the oejskit plugin. It uses classes with funcargs
|
# and we thus have to work a bit to allow this.
|
||||||
# and we thus have to work a bit to allow this.
|
fm = function.session._fixturemanager
|
||||||
fm = function.session._fixturemanager
|
fi = fm.getfixtureinfo(function.parent, function.obj, None)
|
||||||
fi = fm.getfixtureinfo(function.parent, function.obj, None)
|
function._fixtureinfo = fi
|
||||||
function._fixtureinfo = fi
|
request = function._request = FixtureRequest(function)
|
||||||
request = function._request = FixtureRequest(function)
|
request._fillfixtures()
|
||||||
request._fillfixtures()
|
# prune out funcargs for jstests
|
||||||
# prune out funcargs for jstests
|
newfuncargs = {}
|
||||||
newfuncargs = {}
|
for name in fi.argnames:
|
||||||
for name in fi.argnames:
|
newfuncargs[name] = function.funcargs[name]
|
||||||
newfuncargs[name] = function.funcargs[name]
|
function.funcargs = newfuncargs
|
||||||
function.funcargs = newfuncargs
|
else:
|
||||||
else:
|
request._fillfixtures()
|
||||||
request._fillfixtures()
|
|
||||||
|
|
||||||
|
|
||||||
_notexists = object()
|
_notexists = object()
|
||||||
|
@ -630,10 +689,6 @@ class CallSpec2(object):
|
||||||
for arg,val in zip(argnames, valset):
|
for arg,val in zip(argnames, valset):
|
||||||
self._checkargnotcontained(arg)
|
self._checkargnotcontained(arg)
|
||||||
getattr(self, valtype)[arg] = val
|
getattr(self, valtype)[arg] = val
|
||||||
# we want self.params to be always set because of
|
|
||||||
# reorder_items() which groups tests by params/scope
|
|
||||||
if valtype == "funcargs":
|
|
||||||
self.params[arg] = id
|
|
||||||
self.indices[arg] = param_index
|
self.indices[arg] = param_index
|
||||||
self._arg2scopenum[arg] = scopenum
|
self._arg2scopenum[arg] = scopenum
|
||||||
if val is _notexists:
|
if val is _notexists:
|
||||||
|
@ -650,6 +705,8 @@ class CallSpec2(object):
|
||||||
if param is not _notexists:
|
if param is not _notexists:
|
||||||
assert self._globalparam is _notexists
|
assert self._globalparam is _notexists
|
||||||
self._globalparam = param
|
self._globalparam = param
|
||||||
|
for arg in funcargs:
|
||||||
|
self._arg2scopenum[arg] = scopenum_function
|
||||||
|
|
||||||
|
|
||||||
class FuncargnamesCompatAttr:
|
class FuncargnamesCompatAttr:
|
||||||
|
@ -728,7 +785,7 @@ class Metafunc(FuncargnamesCompatAttr):
|
||||||
argvalues = [(_notexists,) * len(argnames)]
|
argvalues = [(_notexists,) * len(argnames)]
|
||||||
|
|
||||||
if scope is None:
|
if scope is None:
|
||||||
scope = "subfunction"
|
scope = "function"
|
||||||
scopenum = scopes.index(scope)
|
scopenum = scopes.index(scope)
|
||||||
if not indirect:
|
if not indirect:
|
||||||
#XXX should we also check for the opposite case?
|
#XXX should we also check for the opposite case?
|
||||||
|
@ -971,30 +1028,26 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr):
|
||||||
for name, val in keywords.items():
|
for name, val in keywords.items():
|
||||||
self.keywords[name] = val
|
self.keywords[name] = val
|
||||||
|
|
||||||
fm = self.session._fixturemanager
|
|
||||||
isyield = self._isyieldedfunction()
|
isyield = self._isyieldedfunction()
|
||||||
self._fixtureinfo = fi = fm.getfixtureinfo(self.parent, self.obj,
|
self._fixtureinfo = fi = self.session._fixturemanager.getfixtureinfo(
|
||||||
self.cls,
|
self.parent, self.obj, self.cls, funcargs=not isyield)
|
||||||
funcargs=not isyield)
|
|
||||||
self.fixturenames = fi.names_closure
|
self.fixturenames = fi.names_closure
|
||||||
if callspec is not None:
|
if callspec is not None:
|
||||||
self.callspec = callspec
|
self.callspec = callspec
|
||||||
self._initrequest()
|
self._initrequest()
|
||||||
|
|
||||||
def _initrequest(self):
|
def _initrequest(self):
|
||||||
|
self.funcargs = {}
|
||||||
if self._isyieldedfunction():
|
if self._isyieldedfunction():
|
||||||
assert not hasattr(self, "callspec"), (
|
assert not hasattr(self, "callspec"), (
|
||||||
"yielded functions (deprecated) cannot have funcargs")
|
"yielded functions (deprecated) cannot have funcargs")
|
||||||
self.funcargs = {}
|
|
||||||
else:
|
else:
|
||||||
if hasattr(self, "callspec"):
|
if hasattr(self, "callspec"):
|
||||||
callspec = self.callspec
|
callspec = self.callspec
|
||||||
self.funcargs = callspec.funcargs.copy()
|
assert not callspec.funcargs
|
||||||
self._genid = callspec.id
|
self._genid = callspec.id
|
||||||
if hasattr(callspec, "param"):
|
if hasattr(callspec, "param"):
|
||||||
self.param = callspec.param
|
self.param = callspec.param
|
||||||
else:
|
|
||||||
self.funcargs = {}
|
|
||||||
self._request = FixtureRequest(self)
|
self._request = FixtureRequest(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1085,9 +1138,10 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
self.fixturename = None
|
self.fixturename = None
|
||||||
#: Scope string, one of "function", "cls", "module", "session"
|
#: Scope string, one of "function", "cls", "module", "session"
|
||||||
self.scope = "function"
|
self.scope = "function"
|
||||||
self._funcargs = self._pyfuncitem.funcargs.copy()
|
self._funcargs = {}
|
||||||
|
self._fixturedefs = {}
|
||||||
fixtureinfo = pyfuncitem._fixtureinfo
|
fixtureinfo = pyfuncitem._fixtureinfo
|
||||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs
|
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
|
||||||
self._arg2index = {}
|
self._arg2index = {}
|
||||||
self.fixturenames = fixtureinfo.names_closure
|
self.fixturenames = fixtureinfo.names_closure
|
||||||
self._fixturemanager = pyfuncitem.session._fixturemanager
|
self._fixturemanager = pyfuncitem.session._fixturemanager
|
||||||
|
@ -1097,15 +1151,17 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
""" underlying collection node (depends on current request scope)"""
|
""" underlying collection node (depends on current request scope)"""
|
||||||
return self._getscopeitem(self.scope)
|
return self._getscopeitem(self.scope)
|
||||||
|
|
||||||
|
|
||||||
def _getnextfixturedef(self, argname):
|
def _getnextfixturedef(self, argname):
|
||||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||||
if fixturedefs is None:
|
if fixturedefs is None:
|
||||||
# we arrive here because of a getfuncargvalue(argname) usage which
|
# we arrive here because of a a dynamic call to
|
||||||
# was naturally not knowable at parsing/collection time
|
# getfuncargvalue(argname) usage which was naturally
|
||||||
|
# not known at parsing/collection time
|
||||||
fixturedefs = self._fixturemanager.getfixturedefs(
|
fixturedefs = self._fixturemanager.getfixturedefs(
|
||||||
argname, self._pyfuncitem.parent.nodeid)
|
argname, self._pyfuncitem.parent.nodeid)
|
||||||
self._arg2fixturedefs[argname] = fixturedefs
|
self._arg2fixturedefs[argname] = fixturedefs
|
||||||
# fixturedefs is immutable so we maintain a decreasing index
|
# fixturedefs list is immutable so we maintain a decreasing index
|
||||||
index = self._arg2index.get(argname, 0) - 1
|
index = self._arg2index.get(argname, 0) - 1
|
||||||
if fixturedefs is None or (-index > len(fixturedefs)):
|
if fixturedefs is None or (-index > len(fixturedefs)):
|
||||||
raise FixtureLookupError(argname, self)
|
raise FixtureLookupError(argname, self)
|
||||||
|
@ -1137,7 +1193,9 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
try:
|
try:
|
||||||
return self._pyfuncitem._testcase
|
return self._pyfuncitem._testcase
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return py.builtin._getimself(self.function)
|
function = getattr(self, "function", None)
|
||||||
|
if function is not None:
|
||||||
|
return py.builtin._getimself(function)
|
||||||
|
|
||||||
@scopeproperty()
|
@scopeproperty()
|
||||||
def module(self):
|
def module(self):
|
||||||
|
@ -1167,12 +1225,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
self._addfinalizer(finalizer, scope=self.scope)
|
self._addfinalizer(finalizer, scope=self.scope)
|
||||||
|
|
||||||
def _addfinalizer(self, finalizer, scope):
|
def _addfinalizer(self, finalizer, scope):
|
||||||
if scope != "function" and hasattr(self, "param"):
|
colitem = self._getscopeitem(scope)
|
||||||
# parametrized resources are sorted by param
|
|
||||||
# so we rather store finalizers per (argname, param)
|
|
||||||
colitem = (self.fixturename, self.param)
|
|
||||||
else:
|
|
||||||
colitem = self._getscopeitem(scope)
|
|
||||||
self._pyfuncitem.session._setupstate.addfinalizer(
|
self._pyfuncitem.session._setupstate.addfinalizer(
|
||||||
finalizer=finalizer, colitem=colitem)
|
finalizer=finalizer, colitem=colitem)
|
||||||
|
|
||||||
|
@ -1246,19 +1299,24 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
setup time, you may use this function to retrieve it inside a fixture
|
setup time, you may use this function to retrieve it inside a fixture
|
||||||
function body.
|
function body.
|
||||||
"""
|
"""
|
||||||
|
return self._get_active_fixturedef(argname).cached_result[0]
|
||||||
|
|
||||||
|
def _get_active_fixturedef(self, argname):
|
||||||
try:
|
try:
|
||||||
return self._funcargs[argname]
|
return self._fixturedefs[argname]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
try:
|
||||||
try:
|
fixturedef = self._getnextfixturedef(argname)
|
||||||
fixturedef = self._getnextfixturedef(argname)
|
except FixtureLookupError:
|
||||||
except FixtureLookupError:
|
if argname == "request":
|
||||||
if argname == "request":
|
class PseudoFixtureDef:
|
||||||
return self
|
cached_result = (self, [0])
|
||||||
raise
|
return PseudoFixtureDef
|
||||||
result = self._getfuncargvalue(fixturedef)
|
raise
|
||||||
self._funcargs[argname] = result
|
result = self._getfuncargvalue(fixturedef)
|
||||||
return result
|
self._funcargs[argname] = result
|
||||||
|
self._fixturedefs[argname] = fixturedef
|
||||||
|
return fixturedef
|
||||||
|
|
||||||
def _get_fixturestack(self):
|
def _get_fixturestack(self):
|
||||||
current = self
|
current = self
|
||||||
|
@ -1272,28 +1330,26 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
current = current._parent_request
|
current = current._parent_request
|
||||||
|
|
||||||
def _getfuncargvalue(self, fixturedef):
|
def _getfuncargvalue(self, fixturedef):
|
||||||
try:
|
|
||||||
return fixturedef.cached_result # set by fixturedef.execute()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
# prepare a subrequest object before calling fixture function
|
# prepare a subrequest object before calling fixture function
|
||||||
# (latter managed by fixturedef)
|
# (latter managed by fixturedef)
|
||||||
argname = fixturedef.argname
|
argname = fixturedef.argname
|
||||||
node = self._pyfuncitem
|
funcitem = self._pyfuncitem
|
||||||
scope = fixturedef.scope
|
scope = fixturedef.scope
|
||||||
try:
|
try:
|
||||||
param = node.callspec.getparam(argname)
|
param = funcitem.callspec.getparam(argname)
|
||||||
except (AttributeError, ValueError):
|
except (AttributeError, ValueError):
|
||||||
param = NOTSET
|
param = NOTSET
|
||||||
|
param_index = 0
|
||||||
else:
|
else:
|
||||||
|
# indices might not be set if old-style metafunc.addcall() was used
|
||||||
|
param_index = funcitem.callspec.indices.get(argname, 0)
|
||||||
# if a parametrize invocation set a scope it will override
|
# if a parametrize invocation set a scope it will override
|
||||||
# the static scope defined with the fixture function
|
# the static scope defined with the fixture function
|
||||||
paramscopenum = node.callspec._arg2scopenum.get(argname)
|
paramscopenum = funcitem.callspec._arg2scopenum.get(argname)
|
||||||
if paramscopenum is not None and \
|
if paramscopenum is not None:
|
||||||
paramscopenum != scopenum_subfunction:
|
|
||||||
scope = scopes[paramscopenum]
|
scope = scopes[paramscopenum]
|
||||||
|
|
||||||
subrequest = SubRequest(self, scope, param, fixturedef)
|
subrequest = SubRequest(self, scope, param, param_index, fixturedef)
|
||||||
|
|
||||||
# check if a higher-level scoped fixture accesses a lower level one
|
# check if a higher-level scoped fixture accesses a lower level one
|
||||||
if scope is not None:
|
if scope is not None:
|
||||||
|
@ -1308,19 +1364,12 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
__tracebackhide__ = False
|
__tracebackhide__ = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# perform the fixture call
|
# call the fixture function
|
||||||
val = fixturedef.execute(request=subrequest)
|
val = fixturedef.execute(request=subrequest)
|
||||||
finally:
|
finally:
|
||||||
# if the fixture function failed it might still have
|
# if fixture function failed it might have registered finalizers
|
||||||
# registered finalizers so we can register
|
|
||||||
# prepare finalization according to scope
|
|
||||||
# (XXX analyse exact finalizing mechanics / cleanup)
|
|
||||||
self.session._setupstate.addfinalizer(fixturedef.finish,
|
self.session._setupstate.addfinalizer(fixturedef.finish,
|
||||||
subrequest.node)
|
subrequest.node)
|
||||||
self._fixturemanager.addargfinalizer(fixturedef.finish, argname)
|
|
||||||
for subargname in fixturedef.argnames: # XXX all deps?
|
|
||||||
self._fixturemanager.addargfinalizer(fixturedef.finish,
|
|
||||||
subargname)
|
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def _factorytraceback(self):
|
def _factorytraceback(self):
|
||||||
|
@ -1336,18 +1385,14 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
|
|
||||||
def _getscopeitem(self, scope):
|
def _getscopeitem(self, scope):
|
||||||
if scope == "function":
|
if scope == "function":
|
||||||
|
# this might also be a non-function Item despite its attribute name
|
||||||
return self._pyfuncitem
|
return self._pyfuncitem
|
||||||
elif scope == "session":
|
node = get_scope_node(self._pyfuncitem, scope)
|
||||||
return self.session
|
if node is None and scope == "class":
|
||||||
elif scope == "class":
|
# fallback to function item itself
|
||||||
x = self._pyfuncitem.getparent(pytest.Class)
|
node = self._pyfuncitem
|
||||||
if x is not None:
|
assert node
|
||||||
return x
|
return node
|
||||||
# fallback to function
|
|
||||||
return self._pyfuncitem
|
|
||||||
if scope == "module":
|
|
||||||
return self._pyfuncitem.getparent(pytest.Module)
|
|
||||||
raise ValueError("unknown finalization scope %r" %(scope,))
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<FixtureRequest for %r>" %(self.node)
|
return "<FixtureRequest for %r>" %(self.node)
|
||||||
|
@ -1356,16 +1401,18 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||||
class SubRequest(FixtureRequest):
|
class SubRequest(FixtureRequest):
|
||||||
""" a sub request for handling getting a fixture from a
|
""" a sub request for handling getting a fixture from a
|
||||||
test function/fixture. """
|
test function/fixture. """
|
||||||
def __init__(self, request, scope, param, fixturedef):
|
def __init__(self, request, scope, param, param_index, fixturedef):
|
||||||
self._parent_request = request
|
self._parent_request = request
|
||||||
self.fixturename = fixturedef.argname
|
self.fixturename = fixturedef.argname
|
||||||
if param is not NOTSET:
|
if param is not NOTSET:
|
||||||
self.param = param
|
self.param = param
|
||||||
|
self.param_index = param_index
|
||||||
self.scope = scope
|
self.scope = scope
|
||||||
self._fixturedef = fixturedef
|
self._fixturedef = fixturedef
|
||||||
self.addfinalizer = fixturedef.addfinalizer
|
self.addfinalizer = fixturedef.addfinalizer
|
||||||
self._pyfuncitem = request._pyfuncitem
|
self._pyfuncitem = request._pyfuncitem
|
||||||
self._funcargs = request._funcargs
|
self._funcargs = request._funcargs
|
||||||
|
self._fixturedefs = request._fixturedefs
|
||||||
self._arg2fixturedefs = request._arg2fixturedefs
|
self._arg2fixturedefs = request._arg2fixturedefs
|
||||||
self._arg2index = request._arg2index
|
self._arg2index = request._arg2index
|
||||||
self.fixturenames = request.fixturenames
|
self.fixturenames = request.fixturenames
|
||||||
|
@ -1380,8 +1427,7 @@ class ScopeMismatchError(Exception):
|
||||||
which has a lower scope (e.g. a Session one calls a function one)
|
which has a lower scope (e.g. a Session one calls a function one)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
scopes = "session module class function subfunction".split()
|
scopes = "session module class function".split()
|
||||||
scopenum_subfunction = scopes.index("subfunction")
|
|
||||||
scopenum_function = scopes.index("function")
|
scopenum_function = scopes.index("function")
|
||||||
def scopemismatch(currentscope, newscope):
|
def scopemismatch(currentscope, newscope):
|
||||||
return scopes.index(newscope) > scopes.index(currentscope)
|
return scopes.index(newscope) > scopes.index(currentscope)
|
||||||
|
@ -1581,14 +1627,14 @@ class FixtureManager:
|
||||||
if argname in arg2fixturedefs:
|
if argname in arg2fixturedefs:
|
||||||
continue
|
continue
|
||||||
fixturedefs = self.getfixturedefs(argname, parentid)
|
fixturedefs = self.getfixturedefs(argname, parentid)
|
||||||
arg2fixturedefs[argname] = fixturedefs
|
|
||||||
if fixturedefs:
|
if fixturedefs:
|
||||||
|
arg2fixturedefs[argname] = fixturedefs
|
||||||
merge(fixturedefs[-1].argnames)
|
merge(fixturedefs[-1].argnames)
|
||||||
return fixturenames_closure, arg2fixturedefs
|
return fixturenames_closure, arg2fixturedefs
|
||||||
|
|
||||||
def pytest_generate_tests(self, metafunc):
|
def pytest_generate_tests(self, metafunc):
|
||||||
for argname in metafunc.fixturenames:
|
for argname in metafunc.fixturenames:
|
||||||
faclist = metafunc._arg2fixturedefs[argname]
|
faclist = metafunc._arg2fixturedefs.get(argname)
|
||||||
if faclist is None:
|
if faclist is None:
|
||||||
continue # will raise FixtureLookupError at setup time
|
continue # will raise FixtureLookupError at setup time
|
||||||
for fixturedef in faclist:
|
for fixturedef in faclist:
|
||||||
|
@ -1600,38 +1646,6 @@ class FixtureManager:
|
||||||
# separate parametrized setups
|
# separate parametrized setups
|
||||||
items[:] = reorder_items(items, set(), {}, 0)
|
items[:] = reorder_items(items, set(), {}, 0)
|
||||||
|
|
||||||
@pytest.mark.trylast
|
|
||||||
def pytest_runtest_teardown(self, item, nextitem):
|
|
||||||
# XXX teardown needs to be normalized for parametrized and
|
|
||||||
# no-parametrized functions
|
|
||||||
try:
|
|
||||||
cs1 = item.callspec
|
|
||||||
except AttributeError:
|
|
||||||
return
|
|
||||||
|
|
||||||
# determine which fixtures are not needed anymore for the next test
|
|
||||||
keylist = []
|
|
||||||
for name in cs1.params:
|
|
||||||
try:
|
|
||||||
if name in nextitem.callspec.params and \
|
|
||||||
cs1.params[name] == nextitem.callspec.params[name]:
|
|
||||||
continue
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
key = (-cs1._arg2scopenum[name], name, cs1.params[name])
|
|
||||||
keylist.append(key)
|
|
||||||
|
|
||||||
# sort by scope (function scope first, then higher ones)
|
|
||||||
keylist.sort()
|
|
||||||
for (scopenum, name, param) in keylist:
|
|
||||||
#if -scopenum >= scopenum_function:
|
|
||||||
# continue # handled by runner.pytest_runtest_teardown
|
|
||||||
item.session._setupstate._callfinalizers((name, param))
|
|
||||||
l = self._arg2finish.pop(name, None)
|
|
||||||
if l is not None:
|
|
||||||
for fin in reversed(l):
|
|
||||||
fin()
|
|
||||||
|
|
||||||
def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
|
def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
|
||||||
if nodeid is not NOTSET:
|
if nodeid is not NOTSET:
|
||||||
holderobj = node_or_obj
|
holderobj = node_or_obj
|
||||||
|
@ -1693,16 +1707,6 @@ class FixtureManager:
|
||||||
if nodeid.startswith(fixturedef.baseid):
|
if nodeid.startswith(fixturedef.baseid):
|
||||||
yield fixturedef
|
yield fixturedef
|
||||||
|
|
||||||
def addargfinalizer(self, finalizer, argname):
|
|
||||||
l = self._arg2finish.setdefault(argname, [])
|
|
||||||
l.append(finalizer)
|
|
||||||
|
|
||||||
def removefinalizer(self, finalizer):
|
|
||||||
for l in self._arg2finish.values():
|
|
||||||
try:
|
|
||||||
l.remove(finalizer)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def fail_fixturefunc(fixturefunc, msg):
|
def fail_fixturefunc(fixturefunc, msg):
|
||||||
fs, lineno = getfslineno(fixturefunc)
|
fs, lineno = getfslineno(fixturefunc)
|
||||||
|
@ -1764,41 +1768,56 @@ class FixtureDef:
|
||||||
while self._finalizer:
|
while self._finalizer:
|
||||||
func = self._finalizer.pop()
|
func = self._finalizer.pop()
|
||||||
func()
|
func()
|
||||||
# check neccesity of next commented call
|
|
||||||
self._fixturemanager.removefinalizer(self.finish)
|
|
||||||
try:
|
try:
|
||||||
del self.cached_result
|
del self.cached_result
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def execute(self, request):
|
def execute(self, request):
|
||||||
|
# get required arguments and register our own finish()
|
||||||
|
# with their finalization
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
for newname in self.argnames:
|
for argname in self.argnames:
|
||||||
kwargs[newname] = request.getfuncargvalue(newname)
|
fixturedef = request._get_active_fixturedef(argname)
|
||||||
|
result, arg_cache_key = fixturedef.cached_result
|
||||||
|
kwargs[argname] = result
|
||||||
|
if argname != "request":
|
||||||
|
fixturedef.addfinalizer(self.finish)
|
||||||
|
|
||||||
|
my_cache_key = request.param_index
|
||||||
|
cached_result = getattr(self, "cached_result", None)
|
||||||
|
if cached_result is not None:
|
||||||
|
#print argname, "Found cached_result", cached_result
|
||||||
|
#print argname, "param_index", param_index
|
||||||
|
result, cache_key = cached_result
|
||||||
|
if my_cache_key == cache_key:
|
||||||
|
#print request.fixturename, "CACHE HIT", repr(my_cache_key)
|
||||||
|
return result
|
||||||
|
#print request.fixturename, "CACHE MISS"
|
||||||
|
# we have a previous but differently parametrized fixture instance
|
||||||
|
# so we need to tear it down before creating a new one
|
||||||
|
self.finish()
|
||||||
|
assert not hasattr(self, "cached_result")
|
||||||
|
|
||||||
if self.unittest:
|
if self.unittest:
|
||||||
result = self.func(request.instance, **kwargs)
|
result = self.func(request.instance, **kwargs)
|
||||||
else:
|
else:
|
||||||
fixturefunc = self.func
|
fixturefunc = self.func
|
||||||
# the fixture function needs to be bound to the actual
|
# the fixture function needs to be bound to the actual
|
||||||
# request.instance so that code working with "self" behaves
|
# request.instance so that code working with "self" behaves
|
||||||
# as expected. XXX request.instance should maybe return None
|
# as expected.
|
||||||
# instead of raising AttributeError
|
if request.instance is not None:
|
||||||
try:
|
fixturefunc = getimfunc(self.func)
|
||||||
if request.instance is not None:
|
if fixturefunc != self.func:
|
||||||
fixturefunc = getimfunc(self.func)
|
fixturefunc = fixturefunc.__get__(request.instance)
|
||||||
if fixturefunc != self.func:
|
|
||||||
fixturefunc = fixturefunc.__get__(request.instance)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
result = call_fixture_func(fixturefunc, request, kwargs,
|
result = call_fixture_func(fixturefunc, request, kwargs,
|
||||||
self.yieldctx)
|
self.yieldctx)
|
||||||
assert not hasattr(self, "cached_result")
|
self.cached_result = (result, my_cache_key)
|
||||||
self.cached_result = result
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return ("<FixtureDef name=%r scope=%r baseid=%r module=%r>" %
|
return ("<FixtureDef name=%r scope=%r baseid=%r >" %
|
||||||
(self.argname, self.scope, self.baseid, self.func.__module__))
|
(self.argname, self.scope, self.baseid))
|
||||||
|
|
||||||
def getfuncargnames(function, startindex=None):
|
def getfuncargnames(function, startindex=None):
|
||||||
# XXX merge with main.py's varnames
|
# XXX merge with main.py's varnames
|
||||||
|
@ -1910,3 +1929,15 @@ def getfixturemarker(obj):
|
||||||
# we don't expect them to be fixture functions
|
# we don't expect them to be fixture functions
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
scopename2class = {
|
||||||
|
'class': Class,
|
||||||
|
'module': Module,
|
||||||
|
'function': pytest.Item,
|
||||||
|
}
|
||||||
|
def get_scope_node(node, scope):
|
||||||
|
cls = scopename2class.get(scope)
|
||||||
|
if cls is None:
|
||||||
|
if scope == "session":
|
||||||
|
return node.session
|
||||||
|
raise ValueError("unknown scope")
|
||||||
|
return node.getparent(cls)
|
||||||
|
|
|
@ -334,7 +334,7 @@ class TestFunction:
|
||||||
def test_function(arg):
|
def test_function(arg):
|
||||||
assert arg.__class__.__name__ == "A"
|
assert arg.__class__.__name__ == "A"
|
||||||
""")
|
""")
|
||||||
reprec = testdir.inline_run()
|
reprec = testdir.inline_run("--fulltrace")
|
||||||
reprec.assertoutcome(passed=1)
|
reprec.assertoutcome(passed=1)
|
||||||
|
|
||||||
def test_parametrize_with_non_hashable_values(self, testdir):
|
def test_parametrize_with_non_hashable_values(self, testdir):
|
||||||
|
|
|
@ -1315,6 +1315,7 @@ class TestAutouseManagement:
|
||||||
l.append("step2-%d" % item)
|
l.append("step2-%d" % item)
|
||||||
|
|
||||||
def test_finish():
|
def test_finish():
|
||||||
|
print (l)
|
||||||
assert l == ["setup-1", "step1-1", "step2-1", "teardown-1",
|
assert l == ["setup-1", "step1-1", "step2-1", "teardown-1",
|
||||||
"setup-2", "step1-2", "step2-2", "teardown-2",]
|
"setup-2", "step1-2", "step2-2", "teardown-2",]
|
||||||
""")
|
""")
|
||||||
|
@ -1683,23 +1684,22 @@ class TestFixtureMarker:
|
||||||
l.append("test3")
|
l.append("test3")
|
||||||
def test_4(modarg, arg):
|
def test_4(modarg, arg):
|
||||||
l.append("test4")
|
l.append("test4")
|
||||||
def test_5():
|
|
||||||
assert len(l) == 12 * 3
|
|
||||||
expected = [
|
|
||||||
'create:1', 'test1', 'fin:1', 'create:2', 'test1',
|
|
||||||
'fin:2', 'create:mod1', 'test2', 'create:1', 'test3',
|
|
||||||
'fin:1', 'create:2', 'test3', 'fin:2', 'create:1',
|
|
||||||
'test4', 'fin:1', 'create:2', 'test4', 'fin:2',
|
|
||||||
'fin:mod1', 'create:mod2', 'test2', 'create:1', 'test3',
|
|
||||||
'fin:1', 'create:2', 'test3', 'fin:2', 'create:1',
|
|
||||||
'test4', 'fin:1', 'create:2', 'test4', 'fin:2',
|
|
||||||
'fin:mod2']
|
|
||||||
import pprint
|
|
||||||
pprint.pprint(list(zip(l, expected)))
|
|
||||||
assert l == expected
|
|
||||||
""")
|
""")
|
||||||
reprec = testdir.inline_run("-v")
|
reprec = testdir.inline_run("-v")
|
||||||
reprec.assertoutcome(passed=12+1)
|
reprec.assertoutcome(passed=12)
|
||||||
|
l = reprec.getcalls("pytest_runtest_call")[0].item.module.l
|
||||||
|
expected = [
|
||||||
|
'create:1', 'test1', 'fin:1', 'create:2', 'test1',
|
||||||
|
'fin:2', 'create:mod1', 'test2', 'create:1', 'test3',
|
||||||
|
'fin:1', 'create:2', 'test3', 'fin:2', 'create:1',
|
||||||
|
'test4', 'fin:1', 'create:2', 'test4', 'fin:2',
|
||||||
|
'fin:mod1', 'create:mod2', 'test2', 'create:1', 'test3',
|
||||||
|
'fin:1', 'create:2', 'test3', 'fin:2', 'create:1',
|
||||||
|
'test4', 'fin:1', 'create:2', 'test4', 'fin:2',
|
||||||
|
'fin:mod2']
|
||||||
|
import pprint
|
||||||
|
pprint.pprint(list(zip(l, expected)))
|
||||||
|
assert l == expected
|
||||||
|
|
||||||
def test_parametrized_fixture_teardown_order(self, testdir):
|
def test_parametrized_fixture_teardown_order(self, testdir):
|
||||||
testdir.makepyfile("""
|
testdir.makepyfile("""
|
||||||
|
@ -1855,7 +1855,6 @@ class TestFixtureMarker:
|
||||||
reprec.assertoutcome(passed=5)
|
reprec.assertoutcome(passed=5)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail
|
|
||||||
@pytest.mark.issue246
|
@pytest.mark.issue246
|
||||||
@pytest.mark.parametrize("scope", ["session", "function", "module"])
|
@pytest.mark.parametrize("scope", ["session", "function", "module"])
|
||||||
def test_finalizer_order_on_parametrization(self, scope, testdir):
|
def test_finalizer_order_on_parametrization(self, scope, testdir):
|
||||||
|
|
|
@ -193,8 +193,8 @@ class TestMetafunc:
|
||||||
metafunc.parametrize('y', [2])
|
metafunc.parametrize('y', [2])
|
||||||
def pytest_funcarg__x(request):
|
def pytest_funcarg__x(request):
|
||||||
return request.param * 10
|
return request.param * 10
|
||||||
def pytest_funcarg__y(request):
|
#def pytest_funcarg__y(request):
|
||||||
return request.param
|
# return request.param
|
||||||
|
|
||||||
def test_simple(x,y):
|
def test_simple(x,y):
|
||||||
assert x in (10,20)
|
assert x in (10,20)
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -115,7 +115,7 @@ commands=
|
||||||
minversion=2.0
|
minversion=2.0
|
||||||
plugins=pytester
|
plugins=pytester
|
||||||
#--pyargs --doctest-modules --ignore=.tox
|
#--pyargs --doctest-modules --ignore=.tox
|
||||||
addopts= -rxs
|
addopts= -rxsX
|
||||||
rsyncdirs=tox.ini pytest.py _pytest testing
|
rsyncdirs=tox.ini pytest.py _pytest testing
|
||||||
python_files=test_*.py *_test.py testing/*/*.py
|
python_files=test_*.py *_test.py testing/*/*.py
|
||||||
python_classes=Test Acceptance
|
python_classes=Test Acceptance
|
||||||
|
|
Loading…
Reference in New Issue