From 4a48a50e3b39020fc29bbe6592153eefe24bc234 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 18 Jun 2009 17:19:12 +0200 Subject: [PATCH] * 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 --- CHANGELOG | 11 +++-- doc/test/extend.txt | 65 ++++++++++++++++++++++--- py/__init__.py | 2 - py/execnet/testing/test_gateway.py | 6 +-- py/test/dist/testing/test_dsession.py | 2 +- py/test/dist/testing/test_nodemanage.py | 2 +- py/test/outcome.py | 30 ------------ py/test/plugin/hookspec.py | 4 ++ py/test/plugin/pytest_execnetcleanup.py | 18 +++---- py/test/plugin/pytest_xfail.py | 17 ++++--- py/test/plugin/test_pytest_runner.py | 10 ---- py/test/pluginmanager.py | 12 +++++ py/test/testing/test_config.py | 3 +- py/test/testing/test_funcargs.py | 12 ----- py/test/testing/test_outcome.py | 31 ------------ py/test/testing/test_pluginmanager.py | 18 ++++++- 16 files changed, 125 insertions(+), 118 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2bc8d69cc..44c47c4ed 100644 --- a/CHANGELOG +++ b/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 ============================================= diff --git a/doc/test/extend.txt b/doc/test/extend.txt index 7cc958f78..eb485fa76 100644 --- a/doc/test/extend.txt +++ b/doc/test/extend.txt @@ -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. diff --git a/py/__init__.py b/py/__init__.py index 8d072a666..285dd5360 100644 --- a/py/__init__.py +++ b/py/__init__.py @@ -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'), diff --git a/py/execnet/testing/test_gateway.py b/py/execnet/testing/test_gateway.py index 50fafa0a7..f5bba5b90 100644 --- a/py/execnet/testing/test_gateway.py +++ b/py/execnet/testing/test_gateway.py @@ -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, diff --git a/py/test/dist/testing/test_dsession.py b/py/test/dist/testing/test_dsession.py index 3a44cb651..542ae9ba8 100644 --- a/py/test/dist/testing/test_dsession.py +++ b/py/test/dist/testing/test_dsession.py @@ -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 diff --git a/py/test/dist/testing/test_nodemanage.py b/py/test/dist/testing/test_nodemanage.py index b62118714..1bc573350 100644 --- a/py/test/dist/testing/test_nodemanage.py +++ b/py/test/dist/testing/test_nodemanage.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]) diff --git a/py/test/outcome.py b/py/test/outcome.py index 45b5ae8b6..e1aa30313 100644 --- a/py/test/outcome.py +++ b/py/test/outcome.py @@ -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 diff --git a/py/test/plugin/hookspec.py b/py/test/plugin/hookspec.py index 04b0849fb..07c65ce99 100644 --- a/py/test/plugin/hookspec.py +++ b/py/test/plugin/hookspec.py @@ -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. """ diff --git a/py/test/plugin/pytest_execnetcleanup.py b/py/test/plugin/pytest_execnetcleanup.py index 1a6dbe6df..7e8ed0408 100644 --- a/py/test/plugin/pytest_execnetcleanup.py +++ b/py/test/plugin/pytest_execnetcleanup.py @@ -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) diff --git a/py/test/plugin/pytest_xfail.py b/py/test/plugin/pytest_xfail.py index 0663580b3..77e9274b0 100644 --- a/py/test/plugin/pytest_xfail.py +++ b/py/test/plugin/pytest_xfail.py @@ -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*", ]) diff --git a/py/test/plugin/test_pytest_runner.py b/py/test/plugin/test_pytest_runner.py index 479a4ca65..3b82de301 100644 --- a/py/test/plugin/test_pytest_runner.py +++ b/py/test/plugin/test_pytest_runner.py @@ -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(): diff --git a/py/test/pluginmanager.py b/py/test/pluginmanager.py index 933085263..5d1ed4954 100644 --- a/py/test/pluginmanager.py +++ b/py/test/pluginmanager.py @@ -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 # diff --git a/py/test/testing/test_config.py b/py/test/testing/test_config.py index 5bae10890..221508d43 100644 --- a/py/test/testing/test_config.py +++ b/py/test/testing/test_config.py @@ -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 diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py index 086ad0d34..1b4b4ea86 100644 --- a/py/test/testing/test_funcargs.py +++ b/py/test/testing/test_funcargs.py @@ -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): diff --git a/py/test/testing/test_outcome.py b/py/test/testing/test_outcome.py index 2ef2e12c2..3c0da6728 100644 --- a/py/test/testing/test_outcome.py +++ b/py/test/testing/test_outcome.py @@ -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" - diff --git a/py/test/testing/test_pluginmanager.py b/py/test/testing/test_pluginmanager.py index a3eb55595..e1d4ec3e8 100644 --- a/py/test/testing/test_pluginmanager.py +++ b/py/test/testing/test_pluginmanager.py @@ -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="""