* introduce and document new pytest_namespace hook
* remove py.test.mark helper * move xfail to work directly on py.test namespace, simplified --HG-- branch : trunk
This commit is contained in:
parent
bcb30d1c7a
commit
4a48a50e3b
11
CHANGELOG
11
CHANGELOG
|
@ -7,10 +7,15 @@ Changes between 1.0.0b1 and 1.0.0b2
|
|||
hooks directly in conftest.py or global pytest_*.py
|
||||
files.
|
||||
|
||||
* documented and refined various hooks
|
||||
* added new pytest_namespace(config) hook that allows
|
||||
to inject helpers directly to the py.test.* namespace.
|
||||
|
||||
* added new style of generative tests via pytest_generate_tests
|
||||
hook
|
||||
* documented and refined many hooks
|
||||
|
||||
* added new style of generative tests via
|
||||
pytest_generate_tests hook that integrates
|
||||
well with function arguments.
|
||||
|
||||
|
||||
Changes between 0.9.2 and 1.0.0b1
|
||||
=============================================
|
||||
|
|
|
@ -8,11 +8,14 @@ py.test implements much of its functionality by calling `well specified
|
|||
hooks`_. Python modules which contain such hook functions are called
|
||||
plugins. Hook functions are discovered in ``conftest.py`` files or
|
||||
in **named** plugins. ``conftest.py`` files are sometimes called "anonymous"
|
||||
or "local" plugins if they define hooks. Named plugins are python modules
|
||||
or packages that have an all lowercase ``pytest_`` prefixed name and who
|
||||
are imported during tool startup or the testing process.
|
||||
or "local" plugins if they contain hooks. They allow to write and distribute
|
||||
some extensions along with the test suite or the application package easily.
|
||||
Named plugins are python modules or packages that have an all lowercase
|
||||
``pytest_`` prefixed name and who are imported during tool startup or
|
||||
the testing process.
|
||||
|
||||
.. _`tool startup`:
|
||||
.. _`test tool starts up`:
|
||||
|
||||
Plugin discovery at tool startup
|
||||
--------------------------------------------
|
||||
|
@ -92,6 +95,53 @@ and minimizes version incompatibilites.
|
|||
|
||||
.. _`original definition of the hook`: http://bitbucket.org/hpk42/py-trunk/src/tip/py/test/plugin/hookspec.py
|
||||
|
||||
.. _`configuration hooks`:
|
||||
|
||||
command line parsing and configuration hooks
|
||||
--------------------------------------------------------------------
|
||||
|
||||
When the `test tool starts up`_ it will invoke all hooks that add
|
||||
command line options in the python standard optparse style.
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
def pytest_addoption(parser):
|
||||
""" add command line options. """"
|
||||
parser.addoption("--myopt", dest="myopt", action="store_true")
|
||||
|
||||
After all these hooks have been called, the command line is parser
|
||||
and a ``config`` object is created and another hook is invoked,
|
||||
for example:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
def pytest_configure(config):
|
||||
config.getvalue("myopt")
|
||||
|
||||
When the test run finishes this corresponding finalizer hook is called:
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
...
|
||||
|
||||
|
||||
adding global py.test helpers and functionality
|
||||
--------------------------------------------------------------------
|
||||
|
||||
If you want to make global helper functions or objects available
|
||||
to your test code you can implement:
|
||||
|
||||
def pytest_namespace(config):
|
||||
""" return dictionary with items to be made available on py.test. """
|
||||
|
||||
All such returned items will be made available directly on
|
||||
the ``py.test`` namespace.
|
||||
|
||||
If you want to provide helpers that are specific to a test function run or need
|
||||
to be setup per test function run, please refer to the `funcargs mechanism`_.
|
||||
|
||||
.. _`funcargs mechanism`: funcargs.html
|
||||
|
||||
|
||||
generic "runtest" hooks
|
||||
------------------------------
|
||||
|
||||
|
@ -179,7 +229,7 @@ the parent node and may be used to access command line
|
|||
options via the ``parent.config`` object.
|
||||
|
||||
|
||||
Python specific test function and module hooks
|
||||
Python test function and module hooks
|
||||
----------------------------------------------------
|
||||
|
||||
For influencing the collection of objects in Python modules
|
||||
|
@ -187,12 +237,11 @@ you can use the following hook:
|
|||
|
||||
.. sourcecode:: python
|
||||
|
||||
pytest_pycollect_makeitem(collector, name, obj)
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
""" return custom item/collector for a python object in a module, or None. """
|
||||
|
||||
This hook will be called for each Python object in a collected
|
||||
Python module. The return value is a custom `collection node`_.
|
||||
|
||||
|
||||
Python module. The return value is a custom `collection node`_ or None.
|
||||
|
||||
.. XXX or ``False`` if you want to indicate that the given item should not be collected.
|
||||
|
||||
|
|
|
@ -66,13 +66,11 @@ initpkg(__name__,
|
|||
'test.__doc__' : ('./test/__init__.py', '__doc__'),
|
||||
'test._PluginManager' : ('./test/pluginmanager.py', 'PluginManager'),
|
||||
'test.raises' : ('./test/outcome.py', 'raises'),
|
||||
'test.mark' : ('./test/outcome.py', 'mark',),
|
||||
'test.deprecated_call' : ('./test/outcome.py', 'deprecated_call'),
|
||||
'test.skip' : ('./test/outcome.py', 'skip'),
|
||||
'test.importorskip' : ('./test/outcome.py', 'importorskip'),
|
||||
'test.fail' : ('./test/outcome.py', 'fail'),
|
||||
'test.exit' : ('./test/outcome.py', 'exit'),
|
||||
'test.pdb' : ('./test/custompdb.py', 'set_trace'),
|
||||
|
||||
# configuration/initialization related test api
|
||||
'test.config' : ('./test/config.py', 'config_per_process'),
|
||||
|
|
|
@ -543,7 +543,7 @@ class TestPopenGateway(PopenGatewayTestSetup, BasicRemoteExecution):
|
|||
ret = channel.receive()
|
||||
assert ret == 42
|
||||
|
||||
@py.test.mark.xfail("fix needed: dying remote process does not cause waitclose() to fail")
|
||||
@py.test.xfail # "fix needed: dying remote process does not cause waitclose() to fail"
|
||||
def test_waitclose_on_remote_killed(self):
|
||||
gw = py.execnet.PopenGateway()
|
||||
channel = gw.remote_exec("""
|
||||
|
@ -616,12 +616,12 @@ class TestSshGateway(BasicRemoteExecution):
|
|||
def test_sshaddress(self):
|
||||
assert self.gw.remoteaddress == self.sshhost
|
||||
|
||||
@py.test.mark.xfail("XXX ssh-gateway error handling")
|
||||
@py.test.xfail # XXX ssh-gateway error handling
|
||||
def test_connexion_failes_on_non_existing_hosts(self):
|
||||
py.test.raises(IOError,
|
||||
"py.execnet.SshGateway('nowhere.codespeak.net')")
|
||||
|
||||
@py.test.mark.xfail("XXX ssh-gateway error handling")
|
||||
@py.test.xfail # "XXX ssh-gateway error handling"
|
||||
def test_deprecated_identity(self):
|
||||
py.test.deprecated_call(
|
||||
py.test.raises, IOError,
|
||||
|
|
|
@ -367,7 +367,7 @@ class TestDSession:
|
|||
assert node.gateway.spec.popen
|
||||
#XXX eq.geteventargs("pytest_sessionfinish")
|
||||
|
||||
@py.test.mark.xfail("test implementation missing")
|
||||
@py.test.xfail
|
||||
def test_collected_function_causes_remote_skip_at_module_level(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
import py
|
||||
|
|
|
@ -11,7 +11,7 @@ class pytest_funcarg__mysetup:
|
|||
request.getfuncargvalue("_pytest")
|
||||
|
||||
class TestNodeManager:
|
||||
@py.test.mark.xfail("consider / forbid implicit rsyncdirs?")
|
||||
@py.test.xfail
|
||||
def test_rsync_roots_no_roots(self, mysetup):
|
||||
mysetup.source.ensure("dir1", "file1").write("hello")
|
||||
config = py.test.config._reparse([source])
|
||||
|
|
|
@ -139,36 +139,6 @@ def deprecated_call(func, *args, **kwargs):
|
|||
raise AssertionError("%r did not produce DeprecationWarning" %(func,))
|
||||
return ret
|
||||
|
||||
class KeywordDecorator:
|
||||
""" decorator for setting function attributes. """
|
||||
def __init__(self, keywords, lastname=None):
|
||||
self._keywords = keywords
|
||||
self._lastname = lastname
|
||||
|
||||
def __call__(self, func=None, **kwargs):
|
||||
if func is None:
|
||||
kw = self._keywords.copy()
|
||||
kw.update(kwargs)
|
||||
return KeywordDecorator(kw)
|
||||
elif not hasattr(func, 'func_dict'):
|
||||
kw = self._keywords.copy()
|
||||
name = self._lastname
|
||||
if name is None:
|
||||
name = "mark"
|
||||
kw[name] = func
|
||||
return KeywordDecorator(kw)
|
||||
func.func_dict.update(self._keywords)
|
||||
return func
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name[0] == "_":
|
||||
raise AttributeError(name)
|
||||
kw = self._keywords.copy()
|
||||
kw[name] = True
|
||||
return self.__class__(kw, lastname=name)
|
||||
|
||||
mark = KeywordDecorator({})
|
||||
|
||||
# exitcodes for the command line
|
||||
EXIT_OK = 0
|
||||
EXIT_TESTSFAILED = 1
|
||||
|
|
|
@ -14,6 +14,10 @@ def pytest_configure(config):
|
|||
``config`` provides access to all such configuration values.
|
||||
"""
|
||||
|
||||
def pytest_namespace(config):
|
||||
""" return dict of name->object to become available at py.test.*"""
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
""" called before test process is exited. """
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ cleanup gateways that were instantiated during a test function run.
|
|||
"""
|
||||
import py
|
||||
|
||||
pytest_plugins = "xfail"
|
||||
|
||||
def pytest_configure(config):
|
||||
config.pluginmanager.register(Execnetcleanup())
|
||||
|
||||
|
@ -37,18 +39,16 @@ class Execnetcleanup:
|
|||
while len(self._gateways) > len(gateways):
|
||||
self._gateways[-1].exit()
|
||||
return res
|
||||
|
||||
@py.test.mark.xfail("clarify plugin registration/unregistration")
|
||||
|
||||
def test_execnetplugin(testdir):
|
||||
p = ExecnetcleanupPlugin()
|
||||
testdir.plugins.append(p)
|
||||
testdir.inline_runsource("""
|
||||
reprec = testdir.inline_runsource("""
|
||||
import py
|
||||
import sys
|
||||
def test_hello():
|
||||
sys._gw = py.execnet.PopenGateway()
|
||||
def test_world():
|
||||
assert hasattr(sys, '_gw')
|
||||
py.test.raises(KeyError, "sys._gw.exit()") # already closed
|
||||
|
||||
""", "-s", "--debug")
|
||||
assert not p._gateways
|
||||
assert py.std.sys._gw
|
||||
py.test.raises(KeyError, "py.std.sys._gw.exit()") # already closed
|
||||
|
||||
reprec.assertoutcome(passed=2)
|
||||
|
|
|
@ -3,7 +3,7 @@ mark tests as expected-to-fail and report them separately.
|
|||
|
||||
example:
|
||||
|
||||
@py.test.mark.xfail("needs refactoring")
|
||||
@py.test.xfail
|
||||
def test_hello():
|
||||
...
|
||||
assert 0
|
||||
|
@ -52,29 +52,34 @@ def pytest_terminal_summary(terminalreporter):
|
|||
for event in xpassed:
|
||||
tr._tw.line("%s: xpassed" %(event.item,))
|
||||
|
||||
def xfail_decorator(func):
|
||||
func.xfail = True
|
||||
return func
|
||||
|
||||
def pytest_namespace(config):
|
||||
return dict(xfail=xfail_decorator)
|
||||
|
||||
# ===============================================================================
|
||||
#
|
||||
# plugin tests
|
||||
#
|
||||
# ===============================================================================
|
||||
|
||||
|
||||
def test_xfail(testdir, linecomp):
|
||||
p = testdir.makepyfile(test_one="""
|
||||
import py
|
||||
pytest_plugins="pytest_xfail",
|
||||
@py.test.mark.xfail
|
||||
@py.test.xfail
|
||||
def test_this():
|
||||
assert 0
|
||||
|
||||
@py.test.mark.xfail
|
||||
@py.test.xfail
|
||||
def test_that():
|
||||
assert 1
|
||||
""")
|
||||
result = testdir.runpytest(p)
|
||||
extra = result.stdout.fnmatch_lines([
|
||||
"*expected failures*",
|
||||
"*test_one.test_this*test_one.py:5*",
|
||||
"*test_one.test_this*test_one.py:4*",
|
||||
"*UNEXPECTEDLY PASSING*",
|
||||
"*test_that*",
|
||||
])
|
||||
|
|
|
@ -29,16 +29,6 @@ class TestSetupState:
|
|||
|
||||
|
||||
class BaseFunctionalTests:
|
||||
def test_funcattr(self, testdir):
|
||||
reports = testdir.runitem("""
|
||||
import py
|
||||
@py.test.mark(xfail="needs refactoring")
|
||||
def test_func():
|
||||
raise Exit()
|
||||
""")
|
||||
rep = reports[1]
|
||||
assert rep.keywords['xfail'] == "needs refactoring"
|
||||
|
||||
def test_passfunction(self, testdir):
|
||||
reports = testdir.runitem("""
|
||||
def test_func():
|
||||
|
|
|
@ -162,16 +162,25 @@ class PluginManager(object):
|
|||
if hasattr(self, '_config'):
|
||||
self.call_plugin(plugin, "pytest_addoption", parser=self._config._parser)
|
||||
self.call_plugin(plugin, "pytest_configure", config=self._config)
|
||||
#dic = self.call_plugin(plugin, "pytest_namespace", config=self._config)
|
||||
#self._updateext(dic)
|
||||
|
||||
def call_plugin(self, plugin, methname, **kwargs):
|
||||
return self.MultiCall(self.listattr(methname, plugins=[plugin]),
|
||||
**kwargs).execute(firstresult=True)
|
||||
|
||||
def _updateext(self, dic):
|
||||
if dic:
|
||||
for name, value in dic.items():
|
||||
setattr(py.test, name, value)
|
||||
|
||||
def do_configure(self, config):
|
||||
assert not hasattr(self, '_config')
|
||||
config.pluginmanager.register(self)
|
||||
self._config = config
|
||||
config.hook.pytest_configure(config=self._config)
|
||||
for dic in config.hook.pytest_namespace(config=config) or []:
|
||||
self._updateext(dic)
|
||||
|
||||
def do_unconfigure(self, config):
|
||||
config = self._config
|
||||
|
@ -179,6 +188,9 @@ class PluginManager(object):
|
|||
config.hook.pytest_unconfigure(config=config)
|
||||
config.pluginmanager.unregister(self)
|
||||
|
||||
class Ext:
|
||||
""" namespace for extension objects. """
|
||||
|
||||
#
|
||||
# XXX old code to automatically load classes
|
||||
#
|
||||
|
|
|
@ -91,6 +91,7 @@ class TestConfigTmpdir:
|
|||
assert config2.basetemp != config3.basetemp
|
||||
|
||||
class TestConfigAPI:
|
||||
|
||||
def test_config_getvalue_honours_conftest(self, testdir):
|
||||
testdir.makepyfile(conftest="x=1")
|
||||
testdir.mkdir("sub").join("conftest.py").write("x=2 ; y = 3")
|
||||
|
@ -320,8 +321,8 @@ def test_options_on_small_file_do_not_blow_up(testdir):
|
|||
def test_default_registry():
|
||||
assert py.test.config.pluginmanager.comregistry is py._com.comregistry
|
||||
|
||||
@py.test.mark.todo("test for deprecation")
|
||||
def test_ensuretemp():
|
||||
# XXX test for deprecation
|
||||
d1 = py.test.ensuretemp('hello')
|
||||
d2 = py.test.ensuretemp('hello')
|
||||
assert d1 == d2
|
||||
|
|
|
@ -200,18 +200,6 @@ class TestRequest:
|
|||
req = funcargs.FuncargRequest(item)
|
||||
assert req.fspath == modcol.fspath
|
||||
|
||||
class TestRequestProtocol:
|
||||
@py.test.mark.xfail
|
||||
def test_protocol(self, testdir):
|
||||
item = testdir.getitem("""
|
||||
def pytest_funcarg_arg1(request): return 1
|
||||
def pytest_funcarg_arg2(request): return 2
|
||||
def test_func(arg1, arg2): pass
|
||||
""")
|
||||
req = funcargs.FuncargRequest(item)
|
||||
req._fillargs()
|
||||
#assert item.funcreq.
|
||||
|
||||
|
||||
class TestRequestCachedSetup:
|
||||
def test_request_cachedsetup(self, testdir):
|
||||
|
|
|
@ -83,34 +83,3 @@ def test_pytest_exit():
|
|||
excinfo = py.code.ExceptionInfo()
|
||||
assert excinfo.errisinstance(KeyboardInterrupt)
|
||||
|
||||
def test_pytest_mark_getattr():
|
||||
from py.__.test.outcome import mark
|
||||
def f(): pass
|
||||
|
||||
mark.hello(f)
|
||||
assert f.hello == True
|
||||
|
||||
mark.hello("test")(f)
|
||||
assert f.hello == "test"
|
||||
|
||||
py.test.raises(AttributeError, "mark._hello")
|
||||
py.test.raises(AttributeError, "mark.__str__")
|
||||
|
||||
def test_pytest_mark_call():
|
||||
from py.__.test.outcome import mark
|
||||
def f(): pass
|
||||
mark(x=3)(f)
|
||||
assert f.x == 3
|
||||
def g(): pass
|
||||
mark(g)
|
||||
assert not g.func_dict
|
||||
|
||||
mark.hello(f)
|
||||
assert f.hello == True
|
||||
|
||||
mark.hello("test")(f)
|
||||
assert f.hello == "test"
|
||||
|
||||
mark("x1")(f)
|
||||
assert f.mark == "x1"
|
||||
|
||||
|
|
|
@ -158,6 +158,22 @@ class TestPytestPluginInteractions:
|
|||
config.parse([])
|
||||
assert not config.option.test123
|
||||
|
||||
def test_do_ext_namespace(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
def pytest_namespace(config):
|
||||
return {'hello': 'world'}
|
||||
""")
|
||||
p = testdir.makepyfile("""
|
||||
from py.test import hello
|
||||
import py
|
||||
def test_hello():
|
||||
assert hello == "world"
|
||||
""")
|
||||
result = testdir.runpytest(p)
|
||||
assert result.stdout.fnmatch_lines([
|
||||
"*1 passed*"
|
||||
])
|
||||
|
||||
def test_do_option_postinitialize(self, testdir):
|
||||
from py.__.test.config import Config
|
||||
config = Config()
|
||||
|
@ -205,7 +221,7 @@ class TestPytestPluginInteractions:
|
|||
assert not pluginmanager.listattr("hello")
|
||||
assert pluginmanager.listattr("x") == [42]
|
||||
|
||||
@py.test.mark(xfail="implement setupcall")
|
||||
@py.test.xfail # setup call methods
|
||||
def test_call_setup_participants(self, testdir):
|
||||
testdir.makepyfile(
|
||||
conftest="""
|
||||
|
|
Loading…
Reference in New Issue