[svn r63845] introduce new _pytest plugin that allows to selectively record
plugin calls and do assertions about them. --HG-- branch : trunk
This commit is contained in:
parent
0300b2109c
commit
b6b060c1d0
20
py/_com.py
20
py/_com.py
|
@ -160,26 +160,32 @@ class PyPlugins:
|
||||||
|
|
||||||
|
|
||||||
class PluginAPI:
|
class PluginAPI:
|
||||||
def __init__(self, apiclass, plugins):
|
def __init__(self, apiclass, plugins=None):
|
||||||
self._apiclass = apiclass
|
self._apiclass = apiclass
|
||||||
|
if plugins is None:
|
||||||
|
plugins = pyplugins
|
||||||
self._plugins = plugins
|
self._plugins = plugins
|
||||||
for name in vars(apiclass):
|
for name, method in vars(apiclass).items():
|
||||||
if name[:2] != "__":
|
if name[:2] != "__":
|
||||||
mm = CallMaker(plugins, name)
|
firstresult = getattr(method, 'firstresult', False)
|
||||||
|
mm = ApiCall(plugins, name, firstresult=firstresult)
|
||||||
setattr(self, name, mm)
|
setattr(self, name, mm)
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<PluginAPI %r %r>" %(self._apiclass, self._plugins)
|
return "<PluginAPI %r %r>" %(self._apiclass, self._plugins)
|
||||||
|
|
||||||
class CallMaker:
|
class ApiCall:
|
||||||
def __init__(self, plugins, name):
|
def __init__(self, plugins, name, firstresult):
|
||||||
self.plugins = plugins
|
self.plugins = plugins
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.firstresult = firstresult
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<MulticallMaker %r %s>" %(self.name, self.plugins)
|
mode = self.firstresult and "firstresult" or "each"
|
||||||
|
return "<ApiCall %r mode=%s %s>" %(self.name, mode, self.plugins)
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
mc = MultiCall(self.plugins.listattr(self.name), *args, **kwargs)
|
mc = MultiCall(self.plugins.listattr(self.name), *args, **kwargs)
|
||||||
return mc.execute()
|
#print "making multicall", self
|
||||||
|
return mc.execute(firstresult=self.firstresult)
|
||||||
|
|
||||||
pyplugins = PyPlugins()
|
pyplugins = PyPlugins()
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pytest_plugins = 'pytest_doctest', 'pytest_pytester' # , 'pytest_restdoc'
|
pytest_plugins = '_pytest doctest pytester'.split()
|
||||||
|
|
||||||
rsyncdirs = ['../doc']
|
rsyncdirs = ['../doc']
|
||||||
rsyncignore = ['c-extension/greenlet/build']
|
rsyncignore = ['c-extension/greenlet/build']
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,12 @@ class GatewayCleanup:
|
||||||
gw.exit()
|
gw.exit()
|
||||||
#gw.join() # should work as well
|
#gw.join() # should work as well
|
||||||
|
|
||||||
|
class ExecnetAPI:
|
||||||
|
def pyexecnet_gateway_init(self, gateway):
|
||||||
|
""" signal initialisation of new gateway. """
|
||||||
|
def pyexecnet_gateway_exit(self, gateway):
|
||||||
|
""" signal exitting of gateway. """
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# Base Gateway (used for both remote and local side)
|
# Base Gateway (used for both remote and local side)
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
|
@ -70,6 +76,12 @@ class Gateway(object):
|
||||||
self._io = io
|
self._io = io
|
||||||
self._channelfactory = ChannelFactory(self, _startcount)
|
self._channelfactory = ChannelFactory(self, _startcount)
|
||||||
self._cleanup.register(self)
|
self._cleanup.register(self)
|
||||||
|
try:
|
||||||
|
from py._com import PluginAPI
|
||||||
|
except ImportError:
|
||||||
|
self.api = ExecnetAPI()
|
||||||
|
else:
|
||||||
|
self.api = PluginAPI(ExecnetAPI)
|
||||||
|
|
||||||
def _initreceive(self, requestqueue=False):
|
def _initreceive(self, requestqueue=False):
|
||||||
if requestqueue:
|
if requestqueue:
|
||||||
|
@ -331,12 +343,7 @@ class Gateway(object):
|
||||||
self._cleanup.unregister(self)
|
self._cleanup.unregister(self)
|
||||||
self._stopexec()
|
self._stopexec()
|
||||||
self._stopsend()
|
self._stopsend()
|
||||||
try:
|
self.api.pyexecnet_gateway_exit(gateway=self)
|
||||||
py._com.pyplugins.notify("gateway_exit", self)
|
|
||||||
except NameError:
|
|
||||||
# XXX on the remote side 'py' is not imported
|
|
||||||
# and so we can't notify
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _remote_redirect(self, stdout=None, stderr=None):
|
def _remote_redirect(self, stdout=None, stderr=None):
|
||||||
""" return a handle representing a redirection of a remote
|
""" return a handle representing a redirection of a remote
|
||||||
|
|
|
@ -41,7 +41,7 @@ class InstallableGateway(gateway.Gateway):
|
||||||
super(InstallableGateway, self).__init__(io=io, _startcount=1)
|
super(InstallableGateway, self).__init__(io=io, _startcount=1)
|
||||||
# XXX we dissallow execution form the other side
|
# XXX we dissallow execution form the other side
|
||||||
self._initreceive(requestqueue=False)
|
self._initreceive(requestqueue=False)
|
||||||
py._com.pyplugins.notify("gateway_init", self)
|
self.api.pyexecnet_gateway_init(gateway=self)
|
||||||
|
|
||||||
def _remote_bootstrap_gateway(self, io, extra=''):
|
def _remote_bootstrap_gateway(self, io, extra=''):
|
||||||
""" return Gateway with a asynchronously remotely
|
""" return Gateway with a asynchronously remotely
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import py
|
import py
|
||||||
pytest_plugins = "pytester"
|
pytest_plugins = "pytester"
|
||||||
|
from py.__.execnet.gateway import ExecnetAPI
|
||||||
|
|
||||||
class TestExecnetEvents:
|
class TestExecnetEvents:
|
||||||
def test_popengateway(self, eventrecorder):
|
def test_popengateway_events(self, _pytest):
|
||||||
|
rec = _pytest.getcallrecorder(ExecnetAPI)
|
||||||
gw = py.execnet.PopenGateway()
|
gw = py.execnet.PopenGateway()
|
||||||
event = eventrecorder.popevent("gateway_init")
|
call = rec.popcall("pyexecnet_gateway_init")
|
||||||
assert event.args[0] == gw
|
assert call.gateway == gw
|
||||||
gw.exit()
|
gw.exit()
|
||||||
event = eventrecorder.popevent("gateway_exit")
|
call = rec.popcall("pyexecnet_gateway_exit")
|
||||||
assert event.args[0] == gw
|
assert call.gateway == gw
|
||||||
|
|
|
@ -250,7 +250,7 @@ class TestPyPluginsEvents:
|
||||||
assert l == [(13, ), {'x':15}]
|
assert l == [(13, ), {'x':15}]
|
||||||
|
|
||||||
|
|
||||||
class TestMulticallMaker:
|
class TestPluginAPI:
|
||||||
def test_happypath(self):
|
def test_happypath(self):
|
||||||
plugins = PyPlugins()
|
plugins = PyPlugins()
|
||||||
class Api:
|
class Api:
|
||||||
|
@ -267,3 +267,22 @@ class TestMulticallMaker:
|
||||||
l = mcm.hello(3)
|
l = mcm.hello(3)
|
||||||
assert l == [4]
|
assert l == [4]
|
||||||
assert not hasattr(mcm, 'world')
|
assert not hasattr(mcm, 'world')
|
||||||
|
|
||||||
|
def test_firstresult(self):
|
||||||
|
plugins = PyPlugins()
|
||||||
|
class Api:
|
||||||
|
def hello(self, arg): pass
|
||||||
|
hello.firstresult = True
|
||||||
|
|
||||||
|
mcm = PluginAPI(apiclass=Api, plugins=plugins)
|
||||||
|
class Plugin:
|
||||||
|
def hello(self, arg):
|
||||||
|
return arg + 1
|
||||||
|
plugins.register(Plugin())
|
||||||
|
res = mcm.hello(3)
|
||||||
|
assert res == 4
|
||||||
|
|
||||||
|
def test_default_plugins(self):
|
||||||
|
class Api: pass
|
||||||
|
mcm = PluginAPI(apiclass=Api)
|
||||||
|
assert mcm._plugins == py._com.pyplugins
|
||||||
|
|
|
@ -48,7 +48,7 @@ class PluginHooks:
|
||||||
# runtest related hooks
|
# runtest related hooks
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
def pytest_pyfunc_call(self, pyfuncitem, args, kwargs):
|
def pytest_pyfunc_call(self, call, pyfuncitem, args, kwargs):
|
||||||
""" return True if we consumed/did the call to the python function item. """
|
""" return True if we consumed/did the call to the python function item. """
|
||||||
|
|
||||||
def pytest_item_makereport(self, item, excinfo, when, outerr):
|
def pytest_item_makereport(self, item, excinfo, when, outerr):
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import py
|
||||||
|
|
||||||
|
class _pytestPlugin:
|
||||||
|
def pytest_funcarg___pytest(self, pyfuncitem):
|
||||||
|
return PytestArg(pyfuncitem)
|
||||||
|
|
||||||
|
class PytestArg:
|
||||||
|
def __init__(self, pyfuncitem):
|
||||||
|
self.pyfuncitem = pyfuncitem
|
||||||
|
|
||||||
|
def getcallrecorder(self, apiclass, pyplugins=None):
|
||||||
|
if pyplugins is None:
|
||||||
|
pyplugins = self.pyfuncitem.config.pytestplugins.pyplugins
|
||||||
|
callrecorder = CallRecorder(pyplugins)
|
||||||
|
callrecorder.start_recording(apiclass)
|
||||||
|
self.pyfuncitem.addfinalizer(callrecorder.finalize)
|
||||||
|
return callrecorder
|
||||||
|
|
||||||
|
|
||||||
|
class ParsedCall:
|
||||||
|
def __init__(self, name, locals):
|
||||||
|
assert '_name' not in locals
|
||||||
|
self.__dict__.update(locals)
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<ParsedCall %r>" %(self.__dict__,)
|
||||||
|
|
||||||
|
class CallRecorder:
|
||||||
|
def __init__(self, pyplugins):
|
||||||
|
self._pyplugins = pyplugins
|
||||||
|
self.calls = []
|
||||||
|
self._recorders = {}
|
||||||
|
|
||||||
|
def start_recording(self, apiclass):
|
||||||
|
assert apiclass not in self._recorders
|
||||||
|
class RecordCalls:
|
||||||
|
_recorder = self
|
||||||
|
for name, method in vars(apiclass).items():
|
||||||
|
if name[0] != "_":
|
||||||
|
setattr(RecordCalls, name, self._getcallparser(method))
|
||||||
|
recorder = RecordCalls()
|
||||||
|
self._recorders[apiclass] = recorder
|
||||||
|
self._pyplugins.register(recorder)
|
||||||
|
|
||||||
|
def finalize(self):
|
||||||
|
for recorder in self._recorders.values():
|
||||||
|
self._pyplugins.unregister(recorder)
|
||||||
|
|
||||||
|
def _getcallparser(self, method):
|
||||||
|
name = method.__name__
|
||||||
|
args, varargs, varkw, default = py.std.inspect.getargspec(method)
|
||||||
|
assert args[0] == "self"
|
||||||
|
fspec = py.std.inspect.formatargspec(args, varargs, varkw, default)
|
||||||
|
# we use exec because we want to have early type
|
||||||
|
# errors on wrong input arguments, using
|
||||||
|
# *args/**kwargs delays this and gives errors
|
||||||
|
# elsewhere
|
||||||
|
exec py.code.compile("""
|
||||||
|
def %(name)s%(fspec)s:
|
||||||
|
self._recorder.calls.append(
|
||||||
|
ParsedCall(%(name)r, locals()))
|
||||||
|
""" % locals())
|
||||||
|
return locals()[name]
|
||||||
|
|
||||||
|
def popcall(self, name):
|
||||||
|
for i, call in py.builtin.enumerate(self.calls):
|
||||||
|
if call._name == name:
|
||||||
|
del self.calls[i]
|
||||||
|
return call
|
||||||
|
raise ValueError("could not find call %r in %r" %(name, self.calls))
|
||||||
|
|
||||||
|
def test_generic(plugintester):
|
||||||
|
plugintester.apicheck(_pytestPlugin)
|
||||||
|
|
||||||
|
def test_callrecorder_basic():
|
||||||
|
pyplugins = py._com.PyPlugins()
|
||||||
|
rec = CallRecorder(pyplugins)
|
||||||
|
class ApiClass:
|
||||||
|
def xyz(self, arg):
|
||||||
|
pass
|
||||||
|
rec.start_recording(ApiClass)
|
||||||
|
pyplugins.call_each("xyz", 123)
|
||||||
|
call = rec.popcall("xyz")
|
||||||
|
assert call.arg == 123
|
||||||
|
assert call._name == "xyz"
|
||||||
|
py.test.raises(ValueError, "rec.popcall('abc')")
|
||||||
|
|
||||||
|
def test_functional(testdir, linecomp):
|
||||||
|
sorter = testdir.inline_runsource("""
|
||||||
|
import py
|
||||||
|
pytest_plugins="_pytest"
|
||||||
|
def test_func(_pytest):
|
||||||
|
class ApiClass:
|
||||||
|
def xyz(self, arg): pass
|
||||||
|
rec = _pytest.getcallrecorder(ApiClass)
|
||||||
|
class Plugin:
|
||||||
|
def xyz(self, arg):
|
||||||
|
return arg + 1
|
||||||
|
rec._pyplugins.register(Plugin())
|
||||||
|
res = rec._pyplugins.call_firstresult("xyz", 41)
|
||||||
|
assert res == 42
|
||||||
|
""")
|
||||||
|
sorter.assertoutcome(passed=1)
|
Loading…
Reference in New Issue