mid-scale refactoring to make request API available directly on items.
This commit was slightly tricky because i want to backward compatibility especially for the oejskit plugin which uses Funcarg-filling for non-Function objects.
This commit is contained in:
parent
227d847216
commit
91b6f2bda8
16
CHANGELOG
16
CHANGELOG
|
@ -1,6 +1,19 @@
|
||||||
Changes between 2.2.4 and 2.2.5.dev
|
Changes between 2.2.4 and 2.3.0.dev
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
- merge FuncargRequest and Item API such that funcarg-functionality
|
||||||
|
is now natively available on the "item" object passed to the various
|
||||||
|
pytest_runtest hooks. This allows more sensitive behaviour
|
||||||
|
of e.g. the pytest-django plugin which previously had no full
|
||||||
|
access to all instantiated funcargs.
|
||||||
|
This internal API re-organisation is a fully backward compatible
|
||||||
|
change: existing factories accepting a "request" object will
|
||||||
|
get a Function "item" object which carries the same API. In fact,
|
||||||
|
the FuncargRequest API (or rather then a ResourceRequestAPI)
|
||||||
|
could be available for all collection and item nodes but this is
|
||||||
|
left for later consideration because it would render the documentation
|
||||||
|
invalid and the "funcarg" naming sounds odd in context of
|
||||||
|
directory, file, class, etc. nodes.
|
||||||
- catch unicode-issues when writing failure representations
|
- catch unicode-issues when writing failure representations
|
||||||
to terminal to prevent the whole session from crashing
|
to terminal to prevent the whole session from crashing
|
||||||
- fix xfail/skip confusion: a skip-mark or an imperative pytest.skip
|
- fix xfail/skip confusion: a skip-mark or an imperative pytest.skip
|
||||||
|
@ -23,6 +36,7 @@ Changes between 2.2.4 and 2.2.5.dev
|
||||||
pytest-django, trial and unittest integration.
|
pytest-django, trial and unittest integration.
|
||||||
|
|
||||||
- reporting refinements:
|
- reporting refinements:
|
||||||
|
|
||||||
- pytest_report_header now receives a "startdir" so that
|
- pytest_report_header now receives a "startdir" so that
|
||||||
you can use startdir.bestrelpath(yourpath) to show
|
you can use startdir.bestrelpath(yourpath) to show
|
||||||
nice relative path
|
nice relative path
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
#
|
#
|
||||||
__version__ = '2.2.5.dev4'
|
__version__ = '2.3.0.dev1'
|
||||||
|
|
|
@ -119,7 +119,7 @@ class CaptureManager:
|
||||||
return "", ""
|
return "", ""
|
||||||
|
|
||||||
def activate_funcargs(self, pyfuncitem):
|
def activate_funcargs(self, pyfuncitem):
|
||||||
if hasattr(pyfuncitem, 'funcargs'):
|
if pyfuncitem.funcargs:
|
||||||
for name, capfuncarg in pyfuncitem.funcargs.items():
|
for name, capfuncarg in pyfuncitem.funcargs.items():
|
||||||
if name in ('capsys', 'capfd'):
|
if name in ('capsys', 'capfd'):
|
||||||
assert not hasattr(self, '_capturing_funcarg')
|
assert not hasattr(self, '_capturing_funcarg')
|
||||||
|
@ -186,7 +186,7 @@ def pytest_funcarg__capsys(request):
|
||||||
captured output available via ``capsys.readouterr()`` method calls
|
captured output available via ``capsys.readouterr()`` method calls
|
||||||
which return a ``(out, err)`` tuple.
|
which return a ``(out, err)`` tuple.
|
||||||
"""
|
"""
|
||||||
if "capfd" in request._funcargs:
|
if "capfd" in request.funcargs:
|
||||||
raise request.LookupError(error_capsysfderror)
|
raise request.LookupError(error_capsysfderror)
|
||||||
return CaptureFuncarg(py.io.StdCapture)
|
return CaptureFuncarg(py.io.StdCapture)
|
||||||
|
|
||||||
|
@ -195,7 +195,7 @@ def pytest_funcarg__capfd(request):
|
||||||
captured output available via ``capsys.readouterr()`` method calls
|
captured output available via ``capsys.readouterr()`` method calls
|
||||||
which return a ``(out, err)`` tuple.
|
which return a ``(out, err)`` tuple.
|
||||||
"""
|
"""
|
||||||
if "capsys" in request._funcargs:
|
if "capsys" in request.funcargs:
|
||||||
raise request.LookupError(error_capsysfderror)
|
raise request.LookupError(error_capsysfderror)
|
||||||
if not hasattr(os, 'dup'):
|
if not hasattr(os, 'dup'):
|
||||||
pytest.skip("capfd funcarg needs os.dup")
|
pytest.skip("capfd funcarg needs os.dup")
|
||||||
|
|
177
_pytest/main.py
177
_pytest/main.py
|
@ -3,6 +3,8 @@
|
||||||
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()
|
||||||
|
|
||||||
# exitcodes for the command line
|
# exitcodes for the command line
|
||||||
|
@ -144,33 +146,162 @@ class HookProxy:
|
||||||
|
|
||||||
def compatproperty(name):
|
def compatproperty(name):
|
||||||
def fget(self):
|
def fget(self):
|
||||||
|
# deprecated - use pytest.name
|
||||||
return getattr(pytest, name)
|
return getattr(pytest, name)
|
||||||
|
|
||||||
return property(fget, None, None,
|
return property(fget)
|
||||||
"deprecated attribute %r, use pytest.%s" % (name, name))
|
|
||||||
|
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 all Nodes in the collection tree.
|
""" base class for Collector and Item the test collection tree.
|
||||||
Collector subclasses have children, Items are terminal nodes."""
|
Collector subclasses have children, Items are terminal nodes."""
|
||||||
|
|
||||||
def __init__(self, name, parent=None, config=None, session=None):
|
def __init__(self, name, parent=None, config=None, session=None):
|
||||||
#: a unique name with the scope of the parent
|
#: a unique name within the scope of the parent node
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
#: the parent collector node.
|
#: the parent collector node.
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
||||||
#: the test config object
|
#: the pytest config object
|
||||||
self.config = config or parent.config
|
self.config = config or parent.config
|
||||||
|
|
||||||
#: the collection this node is part of
|
#: the session this node is part of
|
||||||
self.session = session or parent.session
|
self.session = session or parent.session
|
||||||
|
|
||||||
#: filesystem path where this node was collected from
|
#: filesystem path where this node was collected from (can be None)
|
||||||
self.fspath = getattr(parent, 'fspath', None)
|
self.fspath = getattr(parent, 'fspath', None)
|
||||||
self.ihook = self.session.gethookproxy(self.fspath)
|
|
||||||
|
#: keywords on this node (node name is always contained)
|
||||||
self.keywords = {self.name: True}
|
self.keywords = {self.name: True}
|
||||||
|
|
||||||
|
#: fspath sensitive hook proxy used to call pytest hooks
|
||||||
|
self.ihook = self.session.gethookproxy(self.fspath)
|
||||||
|
|
||||||
Module = compatproperty("Module")
|
Module = compatproperty("Module")
|
||||||
Class = compatproperty("Class")
|
Class = compatproperty("Class")
|
||||||
Instance = compatproperty("Instance")
|
Instance = compatproperty("Instance")
|
||||||
|
@ -178,6 +309,11 @@ 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):
|
||||||
|
@ -193,12 +329,14 @@ class Node(object):
|
||||||
# methods for ordering nodes
|
# methods for ordering nodes
|
||||||
@property
|
@property
|
||||||
def nodeid(self):
|
def nodeid(self):
|
||||||
|
""" a ::-separated string denoting its collection tree address. """
|
||||||
try:
|
try:
|
||||||
return self._nodeid
|
return self._nodeid
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self._nodeid = x = self._makeid()
|
self._nodeid = x = self._makeid()
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
||||||
def _makeid(self):
|
def _makeid(self):
|
||||||
return self.parent.nodeid + "::" + self.name
|
return self.parent.nodeid + "::" + self.name
|
||||||
|
|
||||||
|
@ -338,15 +476,36 @@ 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):
|
class Item(Node, Request):
|
||||||
""" 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, ""
|
||||||
|
|
||||||
|
def applymarker(self, marker):
|
||||||
|
""" Apply a marker to this item. This method is
|
||||||
|
useful if you have several parametrized function
|
||||||
|
and want to mark a single one of them.
|
||||||
|
|
||||||
|
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
|
||||||
|
created by a call to ``py.test.mark.NAME(...)``.
|
||||||
|
"""
|
||||||
|
if not isinstance(marker, pytest.mark.XYZ.__class__):
|
||||||
|
raise ValueError("%r is not a py.test.mark.* object")
|
||||||
|
self.keywords[marker.markname] = marker
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def location(self):
|
def location(self):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -318,7 +318,7 @@ class TmpTestdir:
|
||||||
# used from runner functional tests
|
# used from runner functional tests
|
||||||
item = self.getitem(source)
|
item = self.getitem(source)
|
||||||
# the test class where we are called from wants to provide the runner
|
# the test class where we are called from wants to provide the runner
|
||||||
testclassinstance = py.builtin._getimself(self.request.function)
|
testclassinstance = self.request.instance
|
||||||
runner = testclassinstance.getrunner()
|
runner = testclassinstance.getrunner()
|
||||||
return runner(item)
|
return runner(item)
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,27 @@ import inspect
|
||||||
import sys
|
import sys
|
||||||
import pytest
|
import pytest
|
||||||
from py._code.code import TerminalRepr
|
from py._code.code import TerminalRepr
|
||||||
from _pytest.monkeypatch import monkeypatch
|
from _pytest.main import Request, Item
|
||||||
|
|
||||||
import _pytest
|
import _pytest
|
||||||
cutdir = py.path.local(_pytest.__file__).dirpath()
|
cutdir = py.path.local(_pytest.__file__).dirpath()
|
||||||
|
|
||||||
|
def cached_property(f):
|
||||||
|
"""returns a cached property that is calculated by function f.
|
||||||
|
taken from http://code.activestate.com/recipes/576563-cached-property/"""
|
||||||
|
def get(self):
|
||||||
|
try:
|
||||||
|
return self._property_cache[f]
|
||||||
|
except AttributeError:
|
||||||
|
self._property_cache = {}
|
||||||
|
x = self._property_cache[f] = f(self)
|
||||||
|
return x
|
||||||
|
except KeyError:
|
||||||
|
x = self._property_cache[f] = f(self)
|
||||||
|
return x
|
||||||
|
return property(get)
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
group = parser.getgroup("general")
|
group = parser.getgroup("general")
|
||||||
group.addoption('--funcargs',
|
group.addoption('--funcargs',
|
||||||
|
@ -60,13 +76,21 @@ def pytest_funcarg__pytestconfig(request):
|
||||||
""" the pytest config object with access to command line opts."""
|
""" the pytest config object with access to command line opts."""
|
||||||
return request.config
|
return request.config
|
||||||
|
|
||||||
|
|
||||||
def pytest_pyfunc_call(__multicall__, pyfuncitem):
|
def pytest_pyfunc_call(__multicall__, pyfuncitem):
|
||||||
if not __multicall__.execute():
|
if not __multicall__.execute():
|
||||||
testfunction = pyfuncitem.obj
|
testfunction = pyfuncitem.obj
|
||||||
if pyfuncitem._isyieldedfunction():
|
if pyfuncitem._isyieldedfunction():
|
||||||
testfunction(*pyfuncitem._args)
|
testfunction(*pyfuncitem._args)
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
|
funcargnames = pyfuncitem.funcargnames
|
||||||
|
except AttributeError:
|
||||||
funcargs = pyfuncitem.funcargs
|
funcargs = pyfuncitem.funcargs
|
||||||
|
else:
|
||||||
|
funcargs = {}
|
||||||
|
for name in funcargnames:
|
||||||
|
funcargs[name] = pyfuncitem.funcargs[name]
|
||||||
testfunction(**funcargs)
|
testfunction(**funcargs)
|
||||||
|
|
||||||
def pytest_collect_file(path, parent):
|
def pytest_collect_file(path, parent):
|
||||||
|
@ -110,6 +134,7 @@ def is_generator(func):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class PyobjMixin(object):
|
class PyobjMixin(object):
|
||||||
|
|
||||||
def obj():
|
def obj():
|
||||||
def fget(self):
|
def fget(self):
|
||||||
try:
|
try:
|
||||||
|
@ -232,12 +257,15 @@ class PyCollectorMixin(PyobjMixin, pytest.Collector):
|
||||||
gentesthook.pcall(plugins, metafunc=metafunc)
|
gentesthook.pcall(plugins, metafunc=metafunc)
|
||||||
Function = self._getcustomclass("Function")
|
Function = self._getcustomclass("Function")
|
||||||
if not metafunc._calls:
|
if not metafunc._calls:
|
||||||
return Function(name, parent=self)
|
return Function(name, parent=self,
|
||||||
|
funcargnames=metafunc.funcargnames)
|
||||||
l = []
|
l = []
|
||||||
for callspec in metafunc._calls:
|
for callspec in metafunc._calls:
|
||||||
subname = "%s[%s]" %(name, callspec.id)
|
subname = "%s[%s]" %(name, callspec.id)
|
||||||
function = Function(name=subname, parent=self,
|
function = Function(name=subname, parent=self,
|
||||||
callspec=callspec, callobj=funcobj, keywords={callspec.id:True})
|
callspec=callspec, callobj=funcobj,
|
||||||
|
funcargnames=metafunc.funcargnames,
|
||||||
|
keywords={callspec.id:True})
|
||||||
l.append(function)
|
l.append(function)
|
||||||
return l
|
return l
|
||||||
|
|
||||||
|
@ -256,6 +284,7 @@ def transfer_markers(funcobj, cls, mod):
|
||||||
pytestmark(funcobj)
|
pytestmark(funcobj)
|
||||||
|
|
||||||
class Module(pytest.File, PyCollectorMixin):
|
class Module(pytest.File, PyCollectorMixin):
|
||||||
|
""" Collector for test classes and functions. """
|
||||||
def _getobj(self):
|
def _getobj(self):
|
||||||
return self._memoizedcall('_obj', self._importtestmodule)
|
return self._memoizedcall('_obj', self._importtestmodule)
|
||||||
|
|
||||||
|
@ -303,7 +332,7 @@ class Module(pytest.File, PyCollectorMixin):
|
||||||
self.obj.teardown_module()
|
self.obj.teardown_module()
|
||||||
|
|
||||||
class Class(PyCollectorMixin, pytest.Collector):
|
class Class(PyCollectorMixin, pytest.Collector):
|
||||||
|
""" Collector for test methods. """
|
||||||
def collect(self):
|
def collect(self):
|
||||||
return [self._getcustomclass("Instance")(name="()", parent=self)]
|
return [self._getcustomclass("Instance")(name="()", parent=self)]
|
||||||
|
|
||||||
|
@ -373,7 +402,7 @@ class FunctionMixin(PyobjMixin):
|
||||||
excinfo.traceback = ntraceback.filter()
|
excinfo.traceback = ntraceback.filter()
|
||||||
|
|
||||||
def _repr_failure_py(self, excinfo, style="long"):
|
def _repr_failure_py(self, excinfo, style="long"):
|
||||||
if excinfo.errisinstance(FuncargRequest.LookupError):
|
if excinfo.errisinstance(Request.LookupError):
|
||||||
fspath, lineno, msg = self.reportinfo()
|
fspath, lineno, msg = self.reportinfo()
|
||||||
lines, _ = inspect.getsourcelines(self.obj)
|
lines, _ = inspect.getsourcelines(self.obj)
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
|
@ -445,77 +474,6 @@ class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector):
|
||||||
return name, call, args
|
return name, call, args
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Test Items
|
|
||||||
#
|
|
||||||
_dummy = object()
|
|
||||||
class Function(FunctionMixin, pytest.Item):
|
|
||||||
""" a Function Item is responsible for setting up
|
|
||||||
and executing a Python callable test object.
|
|
||||||
"""
|
|
||||||
_genid = None
|
|
||||||
def __init__(self, name, parent=None, args=None, config=None,
|
|
||||||
callspec=None, callobj=_dummy, keywords=None, session=None):
|
|
||||||
super(Function, self).__init__(name, parent,
|
|
||||||
config=config, session=session)
|
|
||||||
self._args = args
|
|
||||||
if self._isyieldedfunction():
|
|
||||||
assert not callspec, (
|
|
||||||
"yielded functions (deprecated) cannot have funcargs")
|
|
||||||
else:
|
|
||||||
if callspec is not None:
|
|
||||||
self.callspec = callspec
|
|
||||||
self.funcargs = callspec.funcargs or {}
|
|
||||||
self._genid = callspec.id
|
|
||||||
if hasattr(callspec, "param"):
|
|
||||||
self._requestparam = callspec.param
|
|
||||||
else:
|
|
||||||
self.funcargs = {}
|
|
||||||
if callobj is not _dummy:
|
|
||||||
self._obj = callobj
|
|
||||||
self.function = getattr(self.obj, 'im_func', self.obj)
|
|
||||||
self.keywords.update(py.builtin._getfuncdict(self.obj) or {})
|
|
||||||
if keywords:
|
|
||||||
self.keywords.update(keywords)
|
|
||||||
|
|
||||||
def _getobj(self):
|
|
||||||
name = self.name
|
|
||||||
i = name.find("[") # parametrization
|
|
||||||
if i != -1:
|
|
||||||
name = name[:i]
|
|
||||||
return getattr(self.parent.obj, name)
|
|
||||||
|
|
||||||
def _isyieldedfunction(self):
|
|
||||||
return self._args is not None
|
|
||||||
|
|
||||||
def runtest(self):
|
|
||||||
""" execute the underlying test function. """
|
|
||||||
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
super(Function, self).setup()
|
|
||||||
if hasattr(self, 'funcargs'):
|
|
||||||
fillfuncargs(self)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
try:
|
|
||||||
return (self.name == other.name and
|
|
||||||
self._args == other._args and
|
|
||||||
self.parent == other.parent and
|
|
||||||
self.obj == other.obj and
|
|
||||||
getattr(self, '_genid', None) ==
|
|
||||||
getattr(other, '_genid', None)
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self == other
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((self.parent, self.name))
|
|
||||||
|
|
||||||
def hasinit(obj):
|
def hasinit(obj):
|
||||||
init = getattr(obj, '__init__', None)
|
init = getattr(obj, '__init__', None)
|
||||||
if init:
|
if init:
|
||||||
|
@ -535,10 +493,20 @@ def getfuncargnames(function, startindex=None):
|
||||||
return argnames[startindex:-numdefaults]
|
return argnames[startindex:-numdefaults]
|
||||||
return argnames[startindex:]
|
return argnames[startindex:]
|
||||||
|
|
||||||
def fillfuncargs(function):
|
def fillfuncargs(node):
|
||||||
""" fill missing funcargs. """
|
""" fill missing funcargs. """
|
||||||
request = FuncargRequest(pyfuncitem=function)
|
if not isinstance(node, Function):
|
||||||
request._fillfuncargs()
|
node = FuncargRequest(pyfuncitem=node)
|
||||||
|
if node.funcargs is None:
|
||||||
|
node.funcargs = getattr(node, "_funcargs", {})
|
||||||
|
if not isinstance(node, Function) or not node._isyieldedfunction():
|
||||||
|
try:
|
||||||
|
funcargnames = node.funcargnames
|
||||||
|
except AttributeError:
|
||||||
|
funcargnames = getfuncargnames(node.function)
|
||||||
|
if funcargnames:
|
||||||
|
for argname in funcargnames:
|
||||||
|
node.getfuncargvalue(argname)
|
||||||
|
|
||||||
_notexists = object()
|
_notexists = object()
|
||||||
|
|
||||||
|
@ -697,195 +665,6 @@ class IDMaker:
|
||||||
l.append(str(val))
|
l.append(str(val))
|
||||||
return "-".join(l)
|
return "-".join(l)
|
||||||
|
|
||||||
class FuncargRequest:
|
|
||||||
""" A request for function arguments from a test function.
|
|
||||||
|
|
||||||
Note that there is an optional ``param`` attribute in case
|
|
||||||
there was an invocation to metafunc.addcall(param=...).
|
|
||||||
If no such call was done in a ``pytest_generate_tests``
|
|
||||||
hook, the attribute will not be present.
|
|
||||||
"""
|
|
||||||
_argprefix = "pytest_funcarg__"
|
|
||||||
_argname = None
|
|
||||||
|
|
||||||
class LookupError(LookupError):
|
|
||||||
""" error on performing funcarg request. """
|
|
||||||
|
|
||||||
def __init__(self, pyfuncitem):
|
|
||||||
self._pyfuncitem = pyfuncitem
|
|
||||||
if hasattr(pyfuncitem, '_requestparam'):
|
|
||||||
self.param = pyfuncitem._requestparam
|
|
||||||
extra = [obj for obj in (self.module, self.instance) if obj]
|
|
||||||
self._plugins = pyfuncitem.getplugins() + extra
|
|
||||||
self._funcargs = self._pyfuncitem.funcargs.copy()
|
|
||||||
self._name2factory = {}
|
|
||||||
self._currentarg = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def function(self):
|
|
||||||
""" function object of the test invocation. """
|
|
||||||
return self._pyfuncitem.obj
|
|
||||||
|
|
||||||
@property
|
|
||||||
def keywords(self):
|
|
||||||
""" keywords of the test function item.
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
"""
|
|
||||||
return self._pyfuncitem.keywords
|
|
||||||
|
|
||||||
@property
|
|
||||||
def module(self):
|
|
||||||
""" module where the test function was collected. """
|
|
||||||
return self._pyfuncitem.getparent(pytest.Module).obj
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cls(self):
|
|
||||||
""" class (can be None) where the test function was collected. """
|
|
||||||
clscol = self._pyfuncitem.getparent(pytest.Class)
|
|
||||||
if clscol:
|
|
||||||
return clscol.obj
|
|
||||||
@property
|
|
||||||
def instance(self):
|
|
||||||
""" instance (can be None) on which test function was collected. """
|
|
||||||
return py.builtin._getimself(self.function)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def config(self):
|
|
||||||
""" the pytest config object associated with this request. """
|
|
||||||
return self._pyfuncitem.config
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fspath(self):
|
|
||||||
""" the file system path of the test module which collected this test. """
|
|
||||||
return self._pyfuncitem.fspath
|
|
||||||
|
|
||||||
def _fillfuncargs(self):
|
|
||||||
argnames = getfuncargnames(self.function)
|
|
||||||
if argnames:
|
|
||||||
assert not getattr(self._pyfuncitem, '_args', None), (
|
|
||||||
"yielded functions cannot have funcargs")
|
|
||||||
for argname in argnames:
|
|
||||||
if argname not in self._pyfuncitem.funcargs:
|
|
||||||
self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname)
|
|
||||||
|
|
||||||
|
|
||||||
def applymarker(self, marker):
|
|
||||||
""" Apply a marker to a single test function invocation.
|
|
||||||
This method is useful if you don't want to have a keyword/marker
|
|
||||||
on all function invocations.
|
|
||||||
|
|
||||||
:arg marker: a :py:class:`_pytest.mark.MarkDecorator` object
|
|
||||||
created by a call to ``py.test.mark.NAME(...)``.
|
|
||||||
"""
|
|
||||||
if not isinstance(marker, py.test.mark.XYZ.__class__):
|
|
||||||
raise ValueError("%r is not a py.test.mark.* object")
|
|
||||||
self._pyfuncitem.keywords[marker.markname] = marker
|
|
||||||
|
|
||||||
def cached_setup(self, setup, teardown=None, scope="module", extrakey=None):
|
|
||||||
""" Return a testing resource managed by ``setup`` &
|
|
||||||
``teardown`` calls. ``scope`` and ``extrakey`` determine when the
|
|
||||||
``teardown`` function will be called so that subsequent calls to
|
|
||||||
``setup`` would recreate the resource.
|
|
||||||
|
|
||||||
: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 of (funcargname, scope).
|
|
||||||
"""
|
|
||||||
if not hasattr(self.config, '_setupcache'):
|
|
||||||
self.config._setupcache = {} # XXX weakref?
|
|
||||||
cachekey = (self._currentarg, self._getscopeitem(scope), 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._addfinalizer(finalizer, scope=scope)
|
|
||||||
return val
|
|
||||||
|
|
||||||
def getfuncargvalue(self, argname):
|
|
||||||
""" Retrieve a function argument by name for this test
|
|
||||||
function invocation. This allows one function argument factory
|
|
||||||
to call another function argument factory. If there are two
|
|
||||||
funcarg factories for the same test function argument the first
|
|
||||||
factory may use ``getfuncargvalue`` to call the second one and
|
|
||||||
do something additional with the resource.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self._funcargs[argname]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
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._pyfuncitem.callspec.getparam(argname)
|
|
||||||
except (AttributeError, ValueError):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
mp.setattr(self, 'param', param, raising=False)
|
|
||||||
try:
|
|
||||||
self._funcargs[argname] = res = funcargfactory(request=self)
|
|
||||||
finally:
|
|
||||||
mp.undo()
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _getscopeitem(self, scope):
|
|
||||||
if scope == "function":
|
|
||||||
return self._pyfuncitem
|
|
||||||
elif scope == "session":
|
|
||||||
return None
|
|
||||||
elif scope == "class":
|
|
||||||
x = self._pyfuncitem.getparent(pytest.Class)
|
|
||||||
if x is not None:
|
|
||||||
return x
|
|
||||||
scope = "module"
|
|
||||||
if scope == "module":
|
|
||||||
return self._pyfuncitem.getparent(pytest.Module)
|
|
||||||
raise ValueError("unknown finalization scope %r" %(scope,))
|
|
||||||
|
|
||||||
def addfinalizer(self, finalizer):
|
|
||||||
"""add finalizer function to be called after test function
|
|
||||||
finished execution. """
|
|
||||||
self._addfinalizer(finalizer, scope="function")
|
|
||||||
|
|
||||||
def _addfinalizer(self, finalizer, scope):
|
|
||||||
colitem = self._getscopeitem(scope)
|
|
||||||
self._pyfuncitem.session._setupstate.addfinalizer(
|
|
||||||
finalizer=finalizer, colitem=colitem)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<FuncargRequest for %r>" %(self._pyfuncitem)
|
|
||||||
|
|
||||||
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._pyfuncitem.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 showfuncargs(config):
|
def showfuncargs(config):
|
||||||
from _pytest.main import wrap_session
|
from _pytest.main import wrap_session
|
||||||
|
@ -903,8 +682,8 @@ def _showfuncargs_main(config, session):
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
available = []
|
available = []
|
||||||
for name, factory in vars(plugin).items():
|
for name, factory in vars(plugin).items():
|
||||||
if name.startswith(FuncargRequest._argprefix):
|
if name.startswith(Request._argprefix):
|
||||||
name = name[len(FuncargRequest._argprefix):]
|
name = name[len(Request._argprefix):]
|
||||||
if name not in available:
|
if name not in available:
|
||||||
available.append([name, factory])
|
available.append([name, factory])
|
||||||
if available:
|
if available:
|
||||||
|
@ -1009,3 +788,131 @@ class RaisesContext(object):
|
||||||
self.excinfo.__init__(tp)
|
self.excinfo.__init__(tp)
|
||||||
return issubclass(self.excinfo.type, self.ExpectedException)
|
return issubclass(self.excinfo.type, self.ExpectedException)
|
||||||
|
|
||||||
|
#
|
||||||
|
# the basic py.test Function item
|
||||||
|
#
|
||||||
|
_dummy = object()
|
||||||
|
class Function(FunctionMixin, pytest.Item):
|
||||||
|
""" a Function Item is responsible for setting up and executing a
|
||||||
|
Python test function.
|
||||||
|
"""
|
||||||
|
_genid = None
|
||||||
|
def __init__(self, name, parent=None, args=None, config=None,
|
||||||
|
callspec=None, callobj=_dummy, keywords=None,
|
||||||
|
session=None, funcargnames=()):
|
||||||
|
super(Function, self).__init__(name, parent, config=config,
|
||||||
|
session=session)
|
||||||
|
self.funcargnames = funcargnames
|
||||||
|
self._args = args
|
||||||
|
if self._isyieldedfunction():
|
||||||
|
assert not callspec, (
|
||||||
|
"yielded functions (deprecated) cannot have funcargs")
|
||||||
|
else:
|
||||||
|
if callspec is not None:
|
||||||
|
self.callspec = callspec
|
||||||
|
self._funcargs = callspec.funcargs or {}
|
||||||
|
self._genid = callspec.id
|
||||||
|
if hasattr(callspec, "param"):
|
||||||
|
self.param = callspec.param
|
||||||
|
if callobj is not _dummy:
|
||||||
|
self._obj = callobj
|
||||||
|
|
||||||
|
self.keywords.update(py.builtin._getfuncdict(self.obj) or {})
|
||||||
|
if keywords:
|
||||||
|
self.keywords.update(keywords)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def function(self):
|
||||||
|
"underlying python 'function' object"
|
||||||
|
return getattr(self.obj, 'im_func', self.obj)
|
||||||
|
|
||||||
|
def _getobj(self):
|
||||||
|
name = self.name
|
||||||
|
i = name.find("[") # parametrization
|
||||||
|
if i != -1:
|
||||||
|
name = name[:i]
|
||||||
|
return getattr(self.parent.obj, name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _pyfuncitem(self):
|
||||||
|
"(compatonly) for code expecting pytest-2.2 style request objects"
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _isyieldedfunction(self):
|
||||||
|
return getattr(self, "_args", None) is not None
|
||||||
|
|
||||||
|
def runtest(self):
|
||||||
|
""" execute the underlying test function. """
|
||||||
|
self.ihook.pytest_pyfunc_call(pyfuncitem=self)
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
super(Function, self).setup()
|
||||||
|
fillfuncargs(self)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
try:
|
||||||
|
return (self.name == other.name and
|
||||||
|
self._args == other._args and
|
||||||
|
self.parent == other.parent and
|
||||||
|
self.obj == other.obj and
|
||||||
|
getattr(self, '_genid', None) ==
|
||||||
|
getattr(other, '_genid', None)
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self == other
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((self.parent, self.name))
|
||||||
|
|
||||||
|
|
||||||
|
def itemapi_property(name, set=False):
|
||||||
|
prop = getattr(Function, name, None)
|
||||||
|
doc = getattr(prop, "__doc__", None)
|
||||||
|
def get(self):
|
||||||
|
return getattr(self._pyfuncitem, name)
|
||||||
|
if set:
|
||||||
|
def set(self, value):
|
||||||
|
setattr(self._pyfuncitem, name, value)
|
||||||
|
else:
|
||||||
|
set = None
|
||||||
|
return property(get, set, None, doc)
|
||||||
|
|
||||||
|
|
||||||
|
class FuncargRequest(Request):
|
||||||
|
""" (deprecated) helper interactions with a test function invocation.
|
||||||
|
|
||||||
|
Note that there is an optional ``param`` attribute in case
|
||||||
|
there was an invocation to metafunc.addcall(param=...).
|
||||||
|
If no such call was done in a ``pytest_generate_tests``
|
||||||
|
hook, the attribute will not be present.
|
||||||
|
"""
|
||||||
|
def __init__(self, pyfuncitem):
|
||||||
|
self._pyfuncitem = pyfuncitem
|
||||||
|
Request._initattr(self)
|
||||||
|
self.getplugins = self._pyfuncitem.getplugins
|
||||||
|
self.reportinfo = self._pyfuncitem.reportinfo
|
||||||
|
try:
|
||||||
|
self.param = self._pyfuncitem.param
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<FuncargRequest for %r>" % (self._pyfuncitem.name)
|
||||||
|
|
||||||
|
_getscopeitem = itemapi_property("_getscopeitem")
|
||||||
|
funcargs = itemapi_property("funcargs", set=True)
|
||||||
|
keywords = itemapi_property("keywords")
|
||||||
|
module = itemapi_property("module")
|
||||||
|
cls = itemapi_property("cls")
|
||||||
|
instance = itemapi_property("instance")
|
||||||
|
config = itemapi_property("config")
|
||||||
|
session = itemapi_property("session")
|
||||||
|
fspath = itemapi_property("fspath")
|
||||||
|
applymarker = itemapi_property("applymarker")
|
||||||
|
@property
|
||||||
|
def function(self):
|
||||||
|
return self._pyfuncitem.obj
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
""" (disabled by default) create result information in a plain text file. """
|
""" log machine-parseable test session result information in a plain
|
||||||
|
text file.
|
||||||
|
"""
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
|
|
|
@ -54,15 +54,15 @@ def pytest_configure(config):
|
||||||
mp.setattr(config, '_tmpdirhandler', t, raising=False)
|
mp.setattr(config, '_tmpdirhandler', t, raising=False)
|
||||||
mp.setattr(pytest, 'ensuretemp', t.ensuretemp, raising=False)
|
mp.setattr(pytest, 'ensuretemp', t.ensuretemp, raising=False)
|
||||||
|
|
||||||
def pytest_funcarg__tmpdir(request):
|
def pytest_funcarg__tmpdir(item):
|
||||||
"""return a temporary directory path object
|
"""return a temporary directory path object
|
||||||
which is unique to each test function invocation,
|
which is unique to each test function invocation,
|
||||||
created as a sub directory of the base temporary
|
created as a sub directory of the base temporary
|
||||||
directory. The returned object is a `py.path.local`_
|
directory. The returned object is a `py.path.local`_
|
||||||
path object.
|
path object.
|
||||||
"""
|
"""
|
||||||
name = request._pyfuncitem.name
|
name = item.name
|
||||||
name = py.std.re.sub("[\W]", "_", name)
|
name = py.std.re.sub("[\W]", "_", name)
|
||||||
x = request.config._tmpdirhandler.mktemp(name, numbered=True)
|
x = item.config._tmpdirhandler.mktemp(name, numbered=True)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
#
|
#
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = release = "2.2.4.3"
|
version = release = "2.3.0.dev1"
|
||||||
|
|
||||||
import sys, os
|
import sys, os
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ import sys, os
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
#sys.path.insert(0, os.path.abspath('.'))
|
#sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
autodoc_member_order = "bysource"
|
||||||
|
|
||||||
# -- General configuration -----------------------------------------------------
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
# If your documentation needs a minimal Sphinx version, state it here.
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
@ -53,6 +55,7 @@ project = u'pytest'
|
||||||
copyright = u'2011, holger krekel et alii'
|
copyright = u'2011, holger krekel et alii'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
#language = None
|
#language = None
|
||||||
|
@ -78,7 +81,7 @@ exclude_patterns = ['links.inc', '_build', 'naming20.txt', 'test/*',
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
# If true, the current module name will be prepended to all description
|
||||||
# unit titles (such as .. function::).
|
# unit titles (such as .. function::).
|
||||||
#add_module_names = True
|
add_module_names = False
|
||||||
|
|
||||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
# output. They are ignored by default.
|
# output. They are ignored by default.
|
||||||
|
@ -87,6 +90,8 @@ exclude_patterns = ['links.inc', '_build', 'naming20.txt', 'test/*',
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
pygments_style = 'sphinx'
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
# A list of ignored prefixes for module index sorting.
|
||||||
#modindex_common_prefix = []
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
|
|
@ -11,26 +11,27 @@ Injecting objects into test functions (funcargs)
|
||||||
Dependency injection through function arguments
|
Dependency injection through function arguments
|
||||||
=================================================
|
=================================================
|
||||||
|
|
||||||
py.test lets you inject objects into test functions and precisely
|
py.test lets you inject objects into test invocations and precisely
|
||||||
control their life cycle in relation to the test execution. It is
|
control their life cycle in relation to the overall test execution. Moreover,
|
||||||
also possible to run a test function multiple times with different objects.
|
you can run a test function multiple times injecting different objects.
|
||||||
|
|
||||||
The basic mechanism for injecting objects is also called the
|
The basic mechanism for injecting objects is also called the
|
||||||
*funcarg mechanism* because objects are ultimately injected
|
*funcarg mechanism* because objects are ultimately injected
|
||||||
by calling a test function with it as an argument. Unlike the
|
by calling a test function with it as an argument. Unlike the
|
||||||
classical xUnit approach *funcargs* relate more to `Dependency Injection`_
|
classical xUnit approach *funcargs* relate more to `Dependency Injection`_
|
||||||
because they help to de-couple test code from objects required for
|
because they help to de-couple test code from objects required for
|
||||||
them to execute.
|
them to execute. At test writing time you do not need to care for the
|
||||||
|
details of how your required resources are constructed or if they
|
||||||
|
live through a function, class, module or session scope.
|
||||||
|
|
||||||
.. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection
|
.. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection
|
||||||
|
|
||||||
To create a value with which to call a test function a factory function
|
To create a value with which to call a test function a factory function
|
||||||
is called which gets full access to the test function context and can
|
is called which gets full access to the test function context and can
|
||||||
register finalizers or invoke lifecycle-caching helpers. The factory
|
register finalizers or invoke lifecycle-caching helpers. The factory
|
||||||
can be implemented in same test class or test module, or in a
|
can be implemented in same test class or test module, in a
|
||||||
per-directory ``conftest.py`` file or even in an external plugin. This
|
per-directory ``conftest.py`` file or in an external plugin. This
|
||||||
allows full de-coupling of test code and objects needed for test
|
allows total de-coupling of test and setup code.
|
||||||
execution.
|
|
||||||
|
|
||||||
A test function may be invoked multiple times in which case we
|
A test function may be invoked multiple times in which case we
|
||||||
speak of :ref:`parametrized testing <parametrizing-tests>`. This can be
|
speak of :ref:`parametrized testing <parametrizing-tests>`. This can be
|
||||||
|
@ -38,7 +39,7 @@ very useful if you want to test e.g. against different database backends
|
||||||
or with multiple numerical arguments sets and want to reuse the same set
|
or with multiple numerical arguments sets and want to reuse the same set
|
||||||
of test functions.
|
of test functions.
|
||||||
|
|
||||||
py.test comes with :ref:`builtinfuncargs` and there are some refined usages in the examples section.
|
py.test comes with some :ref:`builtinfuncargs` and there are some refined usages in the examples section.
|
||||||
|
|
||||||
.. _funcarg:
|
.. _funcarg:
|
||||||
|
|
||||||
|
@ -55,10 +56,8 @@ Let's look at a simple self-contained test module::
|
||||||
assert myfuncarg == 17
|
assert myfuncarg == 17
|
||||||
|
|
||||||
This test function needs an injected object named ``myfuncarg``.
|
This test function needs an injected object named ``myfuncarg``.
|
||||||
py.test will discover and call the factory named
|
py.test will automatically discover and call the ``pytest_funcarg__myfuncarg``
|
||||||
``pytest_funcarg__myfuncarg`` within the same module in this case.
|
factory. Running the test looks like this::
|
||||||
|
|
||||||
Running the test looks like this::
|
|
||||||
|
|
||||||
$ py.test test_simplefactory.py
|
$ py.test test_simplefactory.py
|
||||||
=========================== test session starts ============================
|
=========================== test session starts ============================
|
||||||
|
@ -79,9 +78,9 @@ Running the test looks like this::
|
||||||
test_simplefactory.py:5: AssertionError
|
test_simplefactory.py:5: AssertionError
|
||||||
========================= 1 failed in 0.01 seconds =========================
|
========================= 1 failed in 0.01 seconds =========================
|
||||||
|
|
||||||
This means that indeed the test function was called with a ``myfuncarg``
|
This shows that the test function was called with a ``myfuncarg``
|
||||||
argument value of ``42`` and the assert fails. Here is how py.test
|
argument value of ``42`` and the assert fails as expected. Here is
|
||||||
comes to call the test function this way:
|
how py.test comes to call the test function this way:
|
||||||
|
|
||||||
1. py.test :ref:`finds <test discovery>` the ``test_function`` because
|
1. py.test :ref:`finds <test discovery>` the ``test_function`` because
|
||||||
of the ``test_`` prefix. The test function needs a function argument
|
of the ``test_`` prefix. The test function needs a function argument
|
||||||
|
@ -99,13 +98,22 @@ Note that if you misspell a function argument or want
|
||||||
to use one that isn't available, you'll see an error
|
to use one that isn't available, you'll see an error
|
||||||
with a list of available function arguments.
|
with a list of available function arguments.
|
||||||
|
|
||||||
|
.. Note::
|
||||||
|
|
||||||
You can always issue::
|
You can always issue::
|
||||||
|
|
||||||
py.test --funcargs test_simplefactory.py
|
py.test --funcargs test_simplefactory.py
|
||||||
|
|
||||||
to see available function arguments (which you can also
|
to see available function arguments.
|
||||||
think of as "resources").
|
|
||||||
|
|
||||||
|
The request object passed to factories
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
Each funcarg factory receives a :py:class:`~_pytest.main.Request` object which
|
||||||
|
provides methods to manage caching and finalization in the context of the
|
||||||
|
test invocation as well as several attributes of the the underlying test item. In fact, as of version pytest-2.3, the request API is implemented on all Item
|
||||||
|
objects and therefore the request object has general :py:class:`Node attributes and methods <_pytest.main.Node>` attributes. This is a backward compatible
|
||||||
|
change so no changes are neccessary for pre-2.3 funcarg factories.
|
||||||
|
|
||||||
.. _`parametrizing tests, generalized`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/
|
.. _`parametrizing tests, generalized`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/
|
||||||
|
|
||||||
|
@ -116,27 +124,6 @@ think of as "resources").
|
||||||
.. _`funcarg factory`:
|
.. _`funcarg factory`:
|
||||||
.. _factory:
|
.. _factory:
|
||||||
|
|
||||||
The funcarg **request** object
|
|
||||||
=============================================
|
|
||||||
|
|
||||||
Each funcarg factory receives a **request** object tied to a specific test
|
|
||||||
function call. A request object is passed to a funcarg factory and provides
|
|
||||||
access to test configuration and context:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.python.FuncargRequest()
|
|
||||||
:members: function,cls,module,keywords,config
|
|
||||||
|
|
||||||
.. _`useful caching and finalization helpers`:
|
|
||||||
|
|
||||||
.. automethod:: FuncargRequest.addfinalizer
|
|
||||||
|
|
||||||
.. automethod:: FuncargRequest.cached_setup
|
|
||||||
|
|
||||||
.. automethod:: FuncargRequest.applymarker
|
|
||||||
|
|
||||||
.. automethod:: FuncargRequest.getfuncargvalue
|
|
||||||
|
|
||||||
|
|
||||||
.. _`test generators`:
|
.. _`test generators`:
|
||||||
.. _`parametrizing-tests`:
|
.. _`parametrizing-tests`:
|
||||||
.. _`parametrized test functions`:
|
.. _`parametrized test functions`:
|
||||||
|
|
|
@ -296,7 +296,6 @@ into interactive debugging when a test failure occurs.
|
||||||
|
|
||||||
The :py:mod:`_pytest.terminal` reported specifically uses
|
The :py:mod:`_pytest.terminal` reported specifically uses
|
||||||
the reporting hook to print information about a test run.
|
the reporting hook to print information about a test run.
|
||||||
|
|
||||||
Collection hooks
|
Collection hooks
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
|
@ -327,37 +326,44 @@ test execution:
|
||||||
|
|
||||||
.. autofunction: pytest_runtest_logreport
|
.. autofunction: pytest_runtest_logreport
|
||||||
|
|
||||||
Reference of important objects involved in hooks
|
Reference of objects involved in hooks
|
||||||
===========================================================
|
===========================================================
|
||||||
|
|
||||||
.. autoclass:: _pytest.config.Config
|
.. autoclass:: _pytest.main.Request()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: _pytest.config.Parser
|
.. autoclass:: _pytest.config.Config()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: _pytest.main.Node(name, parent)
|
.. autoclass:: _pytest.config.Parser()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
..
|
.. autoclass:: _pytest.main.Node()
|
||||||
.. autoclass:: _pytest.main.File(fspath, parent)
|
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: _pytest.main.Item(name, parent)
|
.. autoclass:: _pytest.main.Collector()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.main.Item()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.python.Module()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.python.Class()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.python.Function()
|
||||||
|
:members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.runner.CallInfo()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: _pytest.python.Module(name, parent)
|
.. autoclass:: _pytest.runner.TestReport()
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.python.Class(name, parent)
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.python.Function(name, parent)
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.runner.CallInfo
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. autoclass:: _pytest.runner.TestReport
|
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -24,7 +24,7 @@ def main():
|
||||||
name='pytest',
|
name='pytest',
|
||||||
description='py.test: simple powerful testing with Python',
|
description='py.test: simple powerful testing with Python',
|
||||||
long_description = long_description,
|
long_description = long_description,
|
||||||
version='2.2.5.dev4',
|
version='2.3.0.dev1',
|
||||||
url='http://pytest.org',
|
url='http://pytest.org',
|
||||||
license='MIT license',
|
license='MIT license',
|
||||||
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
|
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
|
||||||
|
|
|
@ -12,33 +12,25 @@ def interpret(expr):
|
||||||
class TestBinReprIntegration:
|
class TestBinReprIntegration:
|
||||||
pytestmark = needsnewassert
|
pytestmark = needsnewassert
|
||||||
|
|
||||||
def pytest_funcarg__hook(self, request):
|
def test_pytest_assertrepr_compare_called(self, testdir):
|
||||||
class MockHook(object):
|
testdir.makeconftest("""
|
||||||
def __init__(self):
|
l = []
|
||||||
self.called = False
|
def pytest_assertrepr_compare(op, left, right):
|
||||||
self.args = tuple()
|
l.append((op, left, right))
|
||||||
self.kwargs = dict()
|
def pytest_funcarg__l(request):
|
||||||
|
return l
|
||||||
def __call__(self, op, left, right):
|
""")
|
||||||
self.called = True
|
testdir.makepyfile("""
|
||||||
self.op = op
|
def test_hello():
|
||||||
self.left = left
|
assert 0 == 1
|
||||||
self.right = right
|
def test_check(l):
|
||||||
mockhook = MockHook()
|
assert l == [("==", 0, 1)]
|
||||||
monkeypatch = request.getfuncargvalue("monkeypatch")
|
""")
|
||||||
monkeypatch.setattr(util, '_reprcompare', mockhook)
|
result = testdir.runpytest("-v")
|
||||||
return mockhook
|
result.stdout.fnmatch_lines([
|
||||||
|
"*test_hello*FAIL*",
|
||||||
def test_pytest_assertrepr_compare_called(self, hook):
|
"*test_check*PASS*",
|
||||||
interpret('assert 0 == 1')
|
])
|
||||||
assert hook.called
|
|
||||||
|
|
||||||
|
|
||||||
def test_pytest_assertrepr_compare_args(self, hook):
|
|
||||||
interpret('assert [0, 1] == [0, 2]')
|
|
||||||
assert hook.op == '=='
|
|
||||||
assert hook.left == [0, 1]
|
|
||||||
assert hook.right == [0, 2]
|
|
||||||
|
|
||||||
def callequal(left, right):
|
def callequal(left, right):
|
||||||
return plugin.pytest_assertrepr_compare('==', left, right)
|
return plugin.pytest_assertrepr_compare('==', left, right)
|
||||||
|
|
|
@ -690,8 +690,8 @@ class TestRequest:
|
||||||
assert val2 == 2
|
assert val2 == 2
|
||||||
val2 = req.getfuncargvalue("other") # see about caching
|
val2 = req.getfuncargvalue("other") # see about caching
|
||||||
assert val2 == 2
|
assert val2 == 2
|
||||||
req._fillfuncargs()
|
pytest._fillfuncargs(item)
|
||||||
assert item.funcargs == {'something': 1}
|
assert item.funcargs == {'something': 1, "other": 2}
|
||||||
|
|
||||||
def test_request_addfinalizer(self, testdir):
|
def test_request_addfinalizer(self, testdir):
|
||||||
item = testdir.getitem("""
|
item = testdir.getitem("""
|
||||||
|
@ -700,9 +700,8 @@ class TestRequest:
|
||||||
request.addfinalizer(lambda: teardownlist.append(1))
|
request.addfinalizer(lambda: teardownlist.append(1))
|
||||||
def test_func(something): pass
|
def test_func(something): pass
|
||||||
""")
|
""")
|
||||||
req = funcargs.FuncargRequest(item)
|
item.session._setupstate.prepare(item)
|
||||||
req._pyfuncitem.session._setupstate.prepare(item) # XXX
|
pytest._fillfuncargs(item)
|
||||||
req._fillfuncargs()
|
|
||||||
# successively check finalization calls
|
# successively check finalization calls
|
||||||
teardownlist = item.getparent(pytest.Module).obj.teardownlist
|
teardownlist = item.getparent(pytest.Module).obj.teardownlist
|
||||||
ss = item.session._setupstate
|
ss = item.session._setupstate
|
||||||
|
@ -799,7 +798,8 @@ class TestRequestCachedSetup:
|
||||||
req3 = funcargs.FuncargRequest(item3)
|
req3 = funcargs.FuncargRequest(item3)
|
||||||
ret3a = req3.cached_setup(setup, scope="class")
|
ret3a = req3.cached_setup(setup, scope="class")
|
||||||
ret3b = req3.cached_setup(setup, scope="class")
|
ret3b = req3.cached_setup(setup, scope="class")
|
||||||
assert ret3a == ret3b == "hello2"
|
assert ret3a == "hello2"
|
||||||
|
assert ret3b == "hello2"
|
||||||
req4 = funcargs.FuncargRequest(item4)
|
req4 = funcargs.FuncargRequest(item4)
|
||||||
ret4 = req4.cached_setup(setup, scope="class")
|
ret4 = req4.cached_setup(setup, scope="class")
|
||||||
assert ret4 == ret3a
|
assert ret4 == ret3a
|
||||||
|
@ -830,11 +830,12 @@ class TestRequestCachedSetup:
|
||||||
ret1 = req1.cached_setup(setup, teardown, scope="function")
|
ret1 = req1.cached_setup(setup, teardown, scope="function")
|
||||||
assert l == ['setup']
|
assert l == ['setup']
|
||||||
# artificial call of finalizer
|
# artificial call of finalizer
|
||||||
req1._pyfuncitem.session._setupstate._callfinalizers(item1)
|
setupstate = req1._pyfuncitem.session._setupstate
|
||||||
|
setupstate._callfinalizers(item1)
|
||||||
assert l == ["setup", "teardown"]
|
assert l == ["setup", "teardown"]
|
||||||
ret2 = req1.cached_setup(setup, teardown, scope="function")
|
ret2 = req1.cached_setup(setup, teardown, scope="function")
|
||||||
assert l == ["setup", "teardown", "setup"]
|
assert l == ["setup", "teardown", "setup"]
|
||||||
req1._pyfuncitem.session._setupstate._callfinalizers(item1)
|
setupstate._callfinalizers(item1)
|
||||||
assert l == ["setup", "teardown", "setup", "teardown"]
|
assert l == ["setup", "teardown", "setup", "teardown"]
|
||||||
|
|
||||||
def test_request_cached_setup_two_args(self, testdir):
|
def test_request_cached_setup_two_args(self, testdir):
|
||||||
|
@ -1092,9 +1093,9 @@ class TestMetafuncFunctional:
|
||||||
def pytest_generate_tests(metafunc):
|
def pytest_generate_tests(metafunc):
|
||||||
metafunc.addcall(param=metafunc)
|
metafunc.addcall(param=metafunc)
|
||||||
|
|
||||||
def pytest_funcarg__metafunc(request):
|
def pytest_funcarg__metafunc(item):
|
||||||
assert request._pyfuncitem._genid == "0"
|
assert item._genid == "0"
|
||||||
return request.param
|
return item.param
|
||||||
|
|
||||||
def test_function(metafunc, pytestconfig):
|
def test_function(metafunc, pytestconfig):
|
||||||
assert metafunc.config == pytestconfig
|
assert metafunc.config == pytestconfig
|
||||||
|
@ -1588,3 +1589,61 @@ def test_issue117_sessionscopeteardown(testdir):
|
||||||
"*3/x*",
|
"*3/x*",
|
||||||
"*ZeroDivisionError*",
|
"*ZeroDivisionError*",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
class TestRequestAPI:
|
||||||
|
def test_addfinalizer_cachedsetup_getfuncargvalue(self, testdir):
|
||||||
|
testdir.makeconftest("""
|
||||||
|
l = []
|
||||||
|
def pytest_runtest_setup(item):
|
||||||
|
item.addfinalizer(lambda: l.append(1))
|
||||||
|
l2 = item.getfuncargvalue("l")
|
||||||
|
assert l2 is l
|
||||||
|
item.cached_setup(lambda: l.append(2), lambda val: l.append(3),
|
||||||
|
scope="function")
|
||||||
|
def pytest_funcarg__l(request):
|
||||||
|
return l
|
||||||
|
""")
|
||||||
|
testdir.makepyfile("""
|
||||||
|
def test_hello():
|
||||||
|
pass
|
||||||
|
def test_hello2(l):
|
||||||
|
assert l == [2, 3, 1, 2]
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*2 passed*",
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_runtest_setup_sees_filled_funcargs(self, testdir):
|
||||||
|
testdir.makeconftest("""
|
||||||
|
def pytest_runtest_setup(item):
|
||||||
|
assert item.funcargs is None
|
||||||
|
""")
|
||||||
|
testdir.makepyfile("""
|
||||||
|
def pytest_funcarg__a(request):
|
||||||
|
return 1
|
||||||
|
def pytest_funcarg__b(request):
|
||||||
|
return request.getfuncargvalue("a") + 1
|
||||||
|
def test_hello(b):
|
||||||
|
assert b == 2
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*1 passed*",
|
||||||
|
])
|
||||||
|
|
||||||
|
result = testdir.makeconftest("""
|
||||||
|
import pytest
|
||||||
|
@pytest.mark.trylast
|
||||||
|
def pytest_runtest_setup(item):
|
||||||
|
assert item.funcargs == {"a": 1, "b": 2}
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
result.stdout.fnmatch_lines([
|
||||||
|
"*1 passed*",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ import py, pytest
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from _pytest.tmpdir import pytest_funcarg__tmpdir, TempdirHandler
|
from _pytest.tmpdir import pytest_funcarg__tmpdir, TempdirHandler
|
||||||
from _pytest.python import FuncargRequest
|
|
||||||
|
|
||||||
def test_funcarg(testdir):
|
def test_funcarg(testdir):
|
||||||
item = testdir.getitem("""
|
item = testdir.getitem("""
|
||||||
|
@ -11,12 +10,12 @@ def test_funcarg(testdir):
|
||||||
metafunc.addcall(id='b')
|
metafunc.addcall(id='b')
|
||||||
def test_func(tmpdir): pass
|
def test_func(tmpdir): pass
|
||||||
""", 'test_func[a]')
|
""", 'test_func[a]')
|
||||||
p = pytest_funcarg__tmpdir(FuncargRequest(item))
|
p = pytest_funcarg__tmpdir(item)
|
||||||
assert p.check()
|
assert p.check()
|
||||||
bn = p.basename.strip("0123456789")
|
bn = p.basename.strip("0123456789")
|
||||||
assert bn.endswith("test_func_a_")
|
assert bn.endswith("test_func_a_")
|
||||||
item.name = "qwe/\\abc"
|
item.name = "qwe/\\abc"
|
||||||
p = pytest_funcarg__tmpdir(FuncargRequest(item))
|
p = pytest_funcarg__tmpdir(item)
|
||||||
assert p.check()
|
assert p.check()
|
||||||
bn = p.basename.strip("0123456789")
|
bn = p.basename.strip("0123456789")
|
||||||
assert bn == "qwe__abc"
|
assert bn == "qwe__abc"
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -24,7 +24,7 @@ basepython=python2.7
|
||||||
deps=pytest-xdist
|
deps=pytest-xdist
|
||||||
commands=
|
commands=
|
||||||
py.test -n3 -rfsxX \
|
py.test -n3 -rfsxX \
|
||||||
--ignore .tox --junitxml={envlogdir}/junit-{envname}.xml []
|
--ignore .tox --junitxml={envlogdir}/junit-{envname}.xml testing
|
||||||
|
|
||||||
[testenv:trial]
|
[testenv:trial]
|
||||||
changedir=.
|
changedir=.
|
||||||
|
|
Loading…
Reference in New Issue