put automatic funcarg_ API to Py*objects only, refine internal subclassing and initialisation logic
This commit is contained in:
parent
66ed2d123a
commit
8adac2878f
|
@ -119,8 +119,9 @@ class CaptureManager:
|
||||||
return "", ""
|
return "", ""
|
||||||
|
|
||||||
def activate_funcargs(self, pyfuncitem):
|
def activate_funcargs(self, pyfuncitem):
|
||||||
if pyfuncitem.funcargs:
|
funcargs = getattr(pyfuncitem, "funcargs", None)
|
||||||
for name, capfuncarg in pyfuncitem.funcargs.items():
|
if funcargs is not None:
|
||||||
|
for name, capfuncarg in funcargs.items():
|
||||||
if name in ('capsys', 'capfd'):
|
if name in ('capsys', 'capfd'):
|
||||||
assert not hasattr(self, '_capturing_funcarg')
|
assert not hasattr(self, '_capturing_funcarg')
|
||||||
self._capturing_funcarg = capfuncarg
|
self._capturing_funcarg = capfuncarg
|
||||||
|
|
151
_pytest/main.py
151
_pytest/main.py
|
@ -3,7 +3,6 @@
|
||||||
import py
|
import py
|
||||||
import pytest, _pytest
|
import pytest, _pytest
|
||||||
import os, sys, imp
|
import os, sys, imp
|
||||||
from _pytest.monkeypatch import monkeypatch
|
|
||||||
|
|
||||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||||
|
|
||||||
|
@ -151,130 +150,6 @@ def compatproperty(name):
|
||||||
|
|
||||||
return property(fget)
|
return property(fget)
|
||||||
|
|
||||||
def pyobj_property(name):
|
|
||||||
def get(self):
|
|
||||||
node = self.getparent(getattr(pytest, name))
|
|
||||||
if node is not None:
|
|
||||||
return node.obj
|
|
||||||
doc = "python %s object this node was collected from (can be None)." % (
|
|
||||||
name.lower(),)
|
|
||||||
return property(get, None, None, doc)
|
|
||||||
|
|
||||||
class Request(object):
|
|
||||||
_argprefix = "pytest_funcarg__"
|
|
||||||
|
|
||||||
class LookupError(LookupError):
|
|
||||||
""" error while performing funcarg factory lookup. """
|
|
||||||
|
|
||||||
def _initattr(self):
|
|
||||||
self._name2factory = {}
|
|
||||||
self._currentarg = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _plugins(self):
|
|
||||||
extra = [obj for obj in (self.module, self.instance) if obj]
|
|
||||||
return self.getplugins() + extra
|
|
||||||
|
|
||||||
def _getscopeitem(self, scope):
|
|
||||||
if scope == "function":
|
|
||||||
return self
|
|
||||||
elif scope == "session":
|
|
||||||
return None
|
|
||||||
elif scope == "class":
|
|
||||||
x = self.getparent(pytest.Class)
|
|
||||||
if x is not None:
|
|
||||||
return x
|
|
||||||
scope = "module"
|
|
||||||
if scope == "module":
|
|
||||||
return self.getparent(pytest.Module)
|
|
||||||
raise ValueError("unknown scope %r" %(scope,))
|
|
||||||
|
|
||||||
def getfuncargvalue(self, argname):
|
|
||||||
""" Retrieve a named function argument value.
|
|
||||||
|
|
||||||
This function looks up a matching factory and invokes
|
|
||||||
it to obtain the return value. The factory receives
|
|
||||||
the same request object and can itself perform recursive
|
|
||||||
calls to this method, effectively allowing to make use of
|
|
||||||
multiple other funcarg values or to decorate values from
|
|
||||||
other name-matching factories.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self.funcargs[argname]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
except TypeError:
|
|
||||||
self.funcargs = getattr(self, "_funcargs", {})
|
|
||||||
if argname not in self._name2factory:
|
|
||||||
self._name2factory[argname] = self.config.pluginmanager.listattr(
|
|
||||||
plugins=self._plugins,
|
|
||||||
attrname=self._argprefix + str(argname)
|
|
||||||
)
|
|
||||||
#else: we are called recursively
|
|
||||||
if not self._name2factory[argname]:
|
|
||||||
self._raiselookupfailed(argname)
|
|
||||||
funcargfactory = self._name2factory[argname].pop()
|
|
||||||
oldarg = self._currentarg
|
|
||||||
mp = monkeypatch()
|
|
||||||
mp.setattr(self, '_currentarg', argname)
|
|
||||||
try:
|
|
||||||
param = self.callspec.getparam(argname)
|
|
||||||
except (AttributeError, ValueError):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
mp.setattr(self, 'param', param, raising=False)
|
|
||||||
try:
|
|
||||||
self.funcargs[argname] = res = funcargfactory(self)
|
|
||||||
finally:
|
|
||||||
mp.undo()
|
|
||||||
return res
|
|
||||||
|
|
||||||
def addfinalizer(self, finalizer):
|
|
||||||
""" add a no-args finalizer function to be called when the underlying
|
|
||||||
node is torn down."""
|
|
||||||
self.session._setupstate.addfinalizer(finalizer, self)
|
|
||||||
|
|
||||||
def cached_setup(self, setup, teardown=None,
|
|
||||||
scope="module", extrakey=None):
|
|
||||||
""" Return a cached testing resource created by ``setup`` &
|
|
||||||
detroyed by a respective ``teardown(resource)`` call.
|
|
||||||
|
|
||||||
:arg teardown: function receiving a previously setup resource.
|
|
||||||
:arg setup: a no-argument function creating a resource.
|
|
||||||
:arg scope: a string value out of ``function``, ``class``, ``module``
|
|
||||||
or ``session`` indicating the caching lifecycle of the resource.
|
|
||||||
:arg extrakey: added to internal caching key.
|
|
||||||
"""
|
|
||||||
if not hasattr(self.config, '_setupcache'):
|
|
||||||
self.config._setupcache = {} # XXX weakref?
|
|
||||||
colitem = self._getscopeitem(scope)
|
|
||||||
cachekey = (self._currentarg, colitem, extrakey)
|
|
||||||
cache = self.config._setupcache
|
|
||||||
try:
|
|
||||||
val = cache[cachekey]
|
|
||||||
except KeyError:
|
|
||||||
val = setup()
|
|
||||||
cache[cachekey] = val
|
|
||||||
if teardown is not None:
|
|
||||||
def finalizer():
|
|
||||||
del cache[cachekey]
|
|
||||||
teardown(val)
|
|
||||||
self.session._setupstate.addfinalizer(finalizer, colitem)
|
|
||||||
return val
|
|
||||||
|
|
||||||
def _raiselookupfailed(self, argname):
|
|
||||||
available = []
|
|
||||||
for plugin in self._plugins:
|
|
||||||
for name in vars(plugin):
|
|
||||||
if name.startswith(self._argprefix):
|
|
||||||
name = name[len(self._argprefix):]
|
|
||||||
if name not in available:
|
|
||||||
available.append(name)
|
|
||||||
fspath, lineno, msg = self.reportinfo()
|
|
||||||
msg = "LookupError: no factory found for function argument %r" % (argname,)
|
|
||||||
msg += "\n available funcargs: %s" %(", ".join(available),)
|
|
||||||
msg += "\n use 'py.test --funcargs [testpath]' for help on them."
|
|
||||||
raise self.LookupError(msg)
|
|
||||||
|
|
||||||
class Node(object):
|
class Node(object):
|
||||||
""" base class for Collector and Item the test collection tree.
|
""" base class for Collector and Item the test collection tree.
|
||||||
|
@ -302,6 +177,12 @@ class Node(object):
|
||||||
#: fspath sensitive hook proxy used to call pytest hooks
|
#: fspath sensitive hook proxy used to call pytest hooks
|
||||||
self.ihook = self.session.gethookproxy(self.fspath)
|
self.ihook = self.session.gethookproxy(self.fspath)
|
||||||
|
|
||||||
|
self.extrainit()
|
||||||
|
|
||||||
|
def extrainit(self):
|
||||||
|
""""extra initialization after Node is initialized. Implemented
|
||||||
|
by some subclasses. """
|
||||||
|
|
||||||
Module = compatproperty("Module")
|
Module = compatproperty("Module")
|
||||||
Class = compatproperty("Class")
|
Class = compatproperty("Class")
|
||||||
Instance = compatproperty("Instance")
|
Instance = compatproperty("Instance")
|
||||||
|
@ -309,11 +190,6 @@ class Node(object):
|
||||||
File = compatproperty("File")
|
File = compatproperty("File")
|
||||||
Item = compatproperty("Item")
|
Item = compatproperty("Item")
|
||||||
|
|
||||||
module = pyobj_property("Module")
|
|
||||||
cls = pyobj_property("Class")
|
|
||||||
instance = pyobj_property("Instance")
|
|
||||||
|
|
||||||
|
|
||||||
def _getcustomclass(self, name):
|
def _getcustomclass(self, name):
|
||||||
cls = getattr(self, name)
|
cls = getattr(self, name)
|
||||||
if cls != getattr(pytest, name):
|
if cls != getattr(pytest, name):
|
||||||
|
@ -476,20 +352,12 @@ class FSCollector(Collector):
|
||||||
class File(FSCollector):
|
class File(FSCollector):
|
||||||
""" base class for collecting tests from a file. """
|
""" base class for collecting tests from a file. """
|
||||||
|
|
||||||
class Item(Node, Request):
|
class Item(Node):
|
||||||
""" a basic test invocation item. Note that for a single function
|
""" a basic test invocation item. Note that for a single function
|
||||||
there might be multiple test invocation items.
|
there might be multiple test invocation items.
|
||||||
"""
|
"""
|
||||||
nextitem = None
|
nextitem = None
|
||||||
|
|
||||||
def __init__(self, name, parent=None, config=None, session=None):
|
|
||||||
super(Item, self).__init__(name, parent, config, session)
|
|
||||||
self._initattr()
|
|
||||||
self.funcargs = None # later set to a dict from fillfuncargs() or
|
|
||||||
# from getfuncargvalue(). Setting it to
|
|
||||||
# None prevents users from performing
|
|
||||||
# "name in item.funcargs" checks too early.
|
|
||||||
|
|
||||||
def reportinfo(self):
|
def reportinfo(self):
|
||||||
return self.fspath, None, ""
|
return self.fspath, None, ""
|
||||||
|
|
||||||
|
@ -532,10 +400,9 @@ class Session(FSCollector):
|
||||||
__module__ = 'builtins' # for py3
|
__module__ = 'builtins' # for py3
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
super(Session, self).__init__(py.path.local(), parent=None,
|
FSCollector.__init__(self, py.path.local(), parent=None,
|
||||||
config=config, session=self)
|
config=config, session=self)
|
||||||
assert self.config.pluginmanager.register(
|
self.config.pluginmanager.register(self, name="session", prepend=True)
|
||||||
self, name="session", prepend=True)
|
|
||||||
self._testsfailed = 0
|
self._testsfailed = 0
|
||||||
self.shouldstop = False
|
self.shouldstop = False
|
||||||
self.trace = config.trace.root.get("collection")
|
self.trace = config.trace.root.get("collection")
|
||||||
|
|
|
@ -4,7 +4,7 @@ import inspect
|
||||||
import sys
|
import sys
|
||||||
import pytest
|
import pytest
|
||||||
from py._code.code import TerminalRepr
|
from py._code.code import TerminalRepr
|
||||||
from _pytest.main import Request, Item
|
from _pytest.monkeypatch import monkeypatch
|
||||||
|
|
||||||
import _pytest
|
import _pytest
|
||||||
cutdir = py.path.local(_pytest.__file__).dirpath()
|
cutdir = py.path.local(_pytest.__file__).dirpath()
|
||||||
|
@ -24,6 +24,135 @@ def cached_property(f):
|
||||||
return x
|
return x
|
||||||
return property(get)
|
return property(get)
|
||||||
|
|
||||||
|
def pyobj_property(name):
|
||||||
|
def get(self):
|
||||||
|
node = self.getparent(getattr(pytest, name))
|
||||||
|
if node is not None:
|
||||||
|
return node.obj
|
||||||
|
doc = "python %s object this node was collected from (can be None)." % (
|
||||||
|
name.lower(),)
|
||||||
|
return property(get, None, None, doc)
|
||||||
|
|
||||||
|
class Request(object):
|
||||||
|
_argprefix = "pytest_funcarg__"
|
||||||
|
|
||||||
|
class LookupError(LookupError):
|
||||||
|
""" error while performing funcarg factory lookup. """
|
||||||
|
|
||||||
|
def extrainit(self):
|
||||||
|
self._name2factory = {}
|
||||||
|
self._currentarg = None
|
||||||
|
self.funcargs = None # later set to a dict from fillfuncargs() or
|
||||||
|
# from getfuncargvalue(). Setting it to
|
||||||
|
# None prevents users from performing
|
||||||
|
# "name in item.funcargs" checks too early.
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _plugins(self):
|
||||||
|
extra = [obj for obj in (self.module, self.instance) if obj]
|
||||||
|
return self.getplugins() + extra
|
||||||
|
|
||||||
|
def _getscopeitem(self, scope):
|
||||||
|
if scope == "function":
|
||||||
|
return self
|
||||||
|
elif scope == "session":
|
||||||
|
return None
|
||||||
|
elif scope == "class":
|
||||||
|
x = self.getparent(pytest.Class)
|
||||||
|
if x is not None:
|
||||||
|
return x
|
||||||
|
scope = "module"
|
||||||
|
if scope == "module":
|
||||||
|
return self.getparent(pytest.Module)
|
||||||
|
raise ValueError("unknown scope %r" %(scope,))
|
||||||
|
|
||||||
|
def getfuncargvalue(self, argname):
|
||||||
|
""" Retrieve a named function argument value.
|
||||||
|
|
||||||
|
This function looks up a matching factory and invokes
|
||||||
|
it to obtain the return value. The factory receives
|
||||||
|
can itself perform recursive calls to this method,
|
||||||
|
either for using multiple other funcarg values under the hood
|
||||||
|
or to decorate values from other factories matching the same name.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.funcargs[argname]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
except TypeError:
|
||||||
|
self.funcargs = getattr(self, "_funcargs", {})
|
||||||
|
if argname not in self._name2factory:
|
||||||
|
self._name2factory[argname] = self.config.pluginmanager.listattr(
|
||||||
|
plugins=self._plugins,
|
||||||
|
attrname=self._argprefix + str(argname)
|
||||||
|
)
|
||||||
|
#else: we are called recursively
|
||||||
|
if not self._name2factory[argname]:
|
||||||
|
self._raiselookupfailed(argname)
|
||||||
|
funcargfactory = self._name2factory[argname].pop()
|
||||||
|
oldarg = self._currentarg
|
||||||
|
mp = monkeypatch()
|
||||||
|
mp.setattr(self, '_currentarg', argname)
|
||||||
|
try:
|
||||||
|
param = self.callspec.getparam(argname)
|
||||||
|
except (AttributeError, ValueError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
mp.setattr(self, 'param', param, raising=False)
|
||||||
|
try:
|
||||||
|
self.funcargs[argname] = res = funcargfactory(self)
|
||||||
|
finally:
|
||||||
|
mp.undo()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def addfinalizer(self, finalizer):
|
||||||
|
""" add a no-args finalizer function to be called when the underlying
|
||||||
|
node is torn down."""
|
||||||
|
self.session._setupstate.addfinalizer(finalizer, self)
|
||||||
|
|
||||||
|
def cached_setup(self, setup, teardown=None,
|
||||||
|
scope="module", extrakey=None):
|
||||||
|
""" Return a cached testing resource created by ``setup`` &
|
||||||
|
detroyed by a respective ``teardown(resource)`` call.
|
||||||
|
|
||||||
|
:arg teardown: function receiving a previously setup resource.
|
||||||
|
:arg setup: a no-argument function creating a resource.
|
||||||
|
:arg scope: a string value out of ``function``, ``class``, ``module``
|
||||||
|
or ``session`` indicating the caching lifecycle of the resource.
|
||||||
|
:arg extrakey: added to internal caching key.
|
||||||
|
"""
|
||||||
|
if not hasattr(self.config, '_setupcache'):
|
||||||
|
self.config._setupcache = {} # XXX weakref?
|
||||||
|
colitem = self._getscopeitem(scope)
|
||||||
|
cachekey = (self._currentarg, colitem, extrakey)
|
||||||
|
cache = self.config._setupcache
|
||||||
|
try:
|
||||||
|
val = cache[cachekey]
|
||||||
|
except KeyError:
|
||||||
|
val = setup()
|
||||||
|
cache[cachekey] = val
|
||||||
|
if teardown is not None:
|
||||||
|
def finalizer():
|
||||||
|
del cache[cachekey]
|
||||||
|
teardown(val)
|
||||||
|
self.session._setupstate.addfinalizer(finalizer, colitem)
|
||||||
|
return val
|
||||||
|
|
||||||
|
def _raiselookupfailed(self, argname):
|
||||||
|
available = []
|
||||||
|
for plugin in self._plugins:
|
||||||
|
for name in vars(plugin):
|
||||||
|
if name.startswith(self._argprefix):
|
||||||
|
name = name[len(self._argprefix):]
|
||||||
|
if name not in available:
|
||||||
|
available.append(name)
|
||||||
|
fspath, lineno, msg = self.reportinfo()
|
||||||
|
msg = "LookupError: no factory found for function argument %r" % (argname,)
|
||||||
|
msg += "\n available funcargs: %s" %(", ".join(available),)
|
||||||
|
msg += "\n use 'py.test --funcargs [testpath]' for help on them."
|
||||||
|
raise self.LookupError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
group = parser.getgroup("general")
|
group = parser.getgroup("general")
|
||||||
|
@ -133,8 +262,12 @@ def is_generator(func):
|
||||||
# assume them to not be generators
|
# assume them to not be generators
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class PyobjMixin(object):
|
class PyobjContext(object):
|
||||||
|
module = pyobj_property("Module")
|
||||||
|
cls = pyobj_property("Class")
|
||||||
|
instance = pyobj_property("Instance")
|
||||||
|
|
||||||
|
class PyobjMixin(Request, PyobjContext):
|
||||||
def obj():
|
def obj():
|
||||||
def fget(self):
|
def fget(self):
|
||||||
try:
|
try:
|
||||||
|
@ -203,7 +336,7 @@ class PyobjMixin(object):
|
||||||
assert isinstance(lineno, int)
|
assert isinstance(lineno, int)
|
||||||
return fspath, lineno, modpath
|
return fspath, lineno, modpath
|
||||||
|
|
||||||
class PyCollectorMixin(PyobjMixin, pytest.Collector):
|
class PyCollector(PyobjMixin, pytest.Collector):
|
||||||
|
|
||||||
def funcnamefilter(self, name):
|
def funcnamefilter(self, name):
|
||||||
for prefix in self.config.getini("python_functions"):
|
for prefix in self.config.getini("python_functions"):
|
||||||
|
@ -283,7 +416,7 @@ def transfer_markers(funcobj, cls, mod):
|
||||||
else:
|
else:
|
||||||
pytestmark(funcobj)
|
pytestmark(funcobj)
|
||||||
|
|
||||||
class Module(pytest.File, PyCollectorMixin):
|
class Module(pytest.File, PyCollector):
|
||||||
""" Collector for test classes and functions. """
|
""" Collector for test classes and functions. """
|
||||||
def _getobj(self):
|
def _getobj(self):
|
||||||
return self._memoizedcall('_obj', self._importtestmodule)
|
return self._memoizedcall('_obj', self._importtestmodule)
|
||||||
|
@ -331,7 +464,7 @@ class Module(pytest.File, PyCollectorMixin):
|
||||||
else:
|
else:
|
||||||
self.obj.teardown_module()
|
self.obj.teardown_module()
|
||||||
|
|
||||||
class Class(PyCollectorMixin, pytest.Collector):
|
class Class(PyCollector):
|
||||||
""" Collector for test methods. """
|
""" Collector for test methods. """
|
||||||
def collect(self):
|
def collect(self):
|
||||||
return [self._getcustomclass("Instance")(name="()", parent=self)]
|
return [self._getcustomclass("Instance")(name="()", parent=self)]
|
||||||
|
@ -350,7 +483,7 @@ class Class(PyCollectorMixin, pytest.Collector):
|
||||||
teardown_class = getattr(teardown_class, '__func__', teardown_class)
|
teardown_class = getattr(teardown_class, '__func__', teardown_class)
|
||||||
teardown_class(self.obj)
|
teardown_class(self.obj)
|
||||||
|
|
||||||
class Instance(PyCollectorMixin, pytest.Collector):
|
class Instance(PyCollector):
|
||||||
def _getobj(self):
|
def _getobj(self):
|
||||||
return self.parent.obj()
|
return self.parent.obj()
|
||||||
|
|
||||||
|
@ -437,7 +570,7 @@ class FuncargLookupErrorRepr(TerminalRepr):
|
||||||
tw.line("%s:%d" % (self.filename, self.firstlineno+1))
|
tw.line("%s:%d" % (self.filename, self.firstlineno+1))
|
||||||
|
|
||||||
|
|
||||||
class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector):
|
class Generator(FunctionMixin, PyCollector):
|
||||||
def collect(self):
|
def collect(self):
|
||||||
# test generators are seen as collectors but they also
|
# test generators are seen as collectors but they also
|
||||||
# invoke setup/teardown on popular request
|
# invoke setup/teardown on popular request
|
||||||
|
@ -882,7 +1015,7 @@ def itemapi_property(name, set=False):
|
||||||
return property(get, set, None, doc)
|
return property(get, set, None, doc)
|
||||||
|
|
||||||
|
|
||||||
class OldFuncargRequest(Request):
|
class OldFuncargRequest(Request, PyobjContext):
|
||||||
""" (deprecated) helper interactions with a test function invocation.
|
""" (deprecated) helper interactions with a test function invocation.
|
||||||
|
|
||||||
Note that there is an optional ``param`` attribute in case
|
Note that there is an optional ``param`` attribute in case
|
||||||
|
@ -892,9 +1025,11 @@ class OldFuncargRequest(Request):
|
||||||
"""
|
"""
|
||||||
def __init__(self, pyfuncitem):
|
def __init__(self, pyfuncitem):
|
||||||
self._pyfuncitem = pyfuncitem
|
self._pyfuncitem = pyfuncitem
|
||||||
Request._initattr(self)
|
Request.extrainit(self)
|
||||||
|
self.funcargs = pyfuncitem.funcargs
|
||||||
self.getplugins = self._pyfuncitem.getplugins
|
self.getplugins = self._pyfuncitem.getplugins
|
||||||
self.reportinfo = self._pyfuncitem.reportinfo
|
self.reportinfo = self._pyfuncitem.reportinfo
|
||||||
|
self.getparent = self._pyfuncitem.getparent
|
||||||
try:
|
try:
|
||||||
self.param = self._pyfuncitem.param
|
self.param = self._pyfuncitem.param
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -906,9 +1041,6 @@ class OldFuncargRequest(Request):
|
||||||
_getscopeitem = itemapi_property("_getscopeitem")
|
_getscopeitem = itemapi_property("_getscopeitem")
|
||||||
funcargs = itemapi_property("funcargs", set=True)
|
funcargs = itemapi_property("funcargs", set=True)
|
||||||
keywords = itemapi_property("keywords")
|
keywords = itemapi_property("keywords")
|
||||||
module = itemapi_property("module")
|
|
||||||
cls = itemapi_property("cls")
|
|
||||||
instance = itemapi_property("instance")
|
|
||||||
config = itemapi_property("config")
|
config = itemapi_property("config")
|
||||||
session = itemapi_property("session")
|
session = itemapi_property("session")
|
||||||
fspath = itemapi_property("fspath")
|
fspath = itemapi_property("fspath")
|
||||||
|
|
|
@ -1647,3 +1647,21 @@ class TestRequestAPI:
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestResourceIntegrationFunctional:
|
||||||
|
def test_parametrize_with_ids(self, testdir):
|
||||||
|
testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
def pytest_generate_tests(metafunc):
|
||||||
|
metafunc.parametrize(("a", "b"), [(1,1), (1,2)],
|
||||||
|
ids=["basic", "advanced"])
|
||||||
|
|
||||||
|
def test_function(a, b):
|
||||||
|
assert a == b
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest("-v")
|
||||||
|
assert result.ret == 1
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*test_function*basic*PASSED",
|
||||||
|
"*test_function*advanced*FAILED",
|
||||||
|
])
|
||||||
|
|
Loading…
Reference in New Issue