* 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:
holger krekel 2009-06-18 17:19:12 +02:00
parent bcb30d1c7a
commit 4a48a50e3b
16 changed files with 125 additions and 118 deletions

View File

@ -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
=============================================

View File

@ -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.

View File

@ -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'),

View File

@ -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,

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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. """

View File

@ -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)

View File

@ -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*",
])

View File

@ -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():

View File

@ -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
#

View File

@ -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

View File

@ -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):

View File

@ -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"

View File

@ -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="""