From commits-noreply at bitbucket.org Wed Aug 1 09:07:59 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 01 Aug 2012 07:07:59 -0000 Subject: [py-svn] commit/pytest: 4 new changesets Message-ID: <20120801070759.23039.87093@bitbucket03.managed.contegix.com> 4 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/07a765c1ea20/ changeset: 07a765c1ea20 user: hpk42 date: 2012-07-30 10:46:03 summary: minimize active parametrized non-function scoped resources by - re-ordering at collection time - modifying setup/teardown affected #: 9 files diff -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a -r 07a765c1ea206c7271722f7e825e96c816c1d61a _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.0.dev4' +__version__ = '2.3.0.dev5' diff -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a -r 07a765c1ea206c7271722f7e825e96c816c1d61a _pytest/impl --- a/_pytest/impl +++ b/_pytest/impl @@ -1,3 +1,186 @@ + +the new @setup functions +-------------------------------------- + +Consider a given @setup-marked function:: + + @pytest.mark.setup(maxscope=SCOPE) + def mysetup(request, arg1, arg2, ...) + ... + request.addfinalizer(fin) + ... + +then FUNCARGSET denotes the set of (arg1, arg2, ...) funcargs and +all of its dependent funcargs. The mysetup function will execute +for any matching test item once per scope. + +The scope is determined as the minimum scope of all scopes of the args +in FUNCARGSET and the given "maxscope". + +If mysetup has been called and no finalizers have been called it is +called "active". + +Furthermore the following rules apply: + +- if an arg value in FUNCARGSET is about to be torn down, the + mysetup-registered finalizers will execute as well. + +- There will never be two active mysetup invocations. + +Example 1, session scope:: + + @pytest.mark.funcarg(scope="session", params=[1,2]) + def db(request): + request.addfinalizer(db_finalize) + + @pytest.mark.setup + def mysetup(request, db): + request.addfinalizer(mysetup_finalize) + ... + +And a given test module: + + def test_something(): + ... + def test_otherthing(): + pass + +Here is what happens:: + + db(request) executes with request.param == 1 + mysetup(request, db) executes + test_something() executes + test_otherthing() executes + mysetup_finalize() executes + db_finalize() executes + db(request) executes with request.param == 2 + mysetup(request, db) executes + test_something() executes + test_otherthing() executes + mysetup_finalize() executes + db_finalize() executes + +Example 2, session/function scope:: + + @pytest.mark.funcarg(scope="session", params=[1,2]) + def db(request): + request.addfinalizer(db_finalize) + + @pytest.mark.setup(scope="function") + def mysetup(request, db): + ... + request.addfinalizer(mysetup_finalize) + ... + +And a given test module: + + def test_something(): + ... + def test_otherthing(): + pass + +Here is what happens:: + + db(request) executes with request.param == 1 + mysetup(request, db) executes + test_something() executes + mysetup_finalize() executes + mysetup(request, db) executes + test_otherthing() executes + mysetup_finalize() executes + db_finalize() executes + db(request) executes with request.param == 2 + mysetup(request, db) executes + test_something() executes + mysetup_finalize() executes + mysetup(request, db) executes + test_otherthing() executes + mysetup_finalize() executes + db_finalize() executes + + +Example 3 - funcargs session-mix +---------------------------------------- + +Similar with funcargs, an example:: + + @pytest.mark.funcarg(scope="session", params=[1,2]) + def db(request): + request.addfinalizer(db_finalize) + + @pytest.mark.funcarg(scope="function") + def table(request, db): + ... + request.addfinalizer(table_finalize) + ... + +And a given test module: + + def test_something(table): + ... + def test_otherthing(table): + pass + def test_thirdthing(): + pass + +Here is what happens:: + + db(request) executes with param == 1 + table(request, db) + test_something(table) + table_finalize() + table(request, db) + test_otherthing(table) + table_finalize() + db_finalize + db(request) executes with param == 2 + table(request, db) + test_something(table) + table_finalize() + table(request, db) + test_otherthing(table) + table_finalize() + db_finalize + test_thirdthing() + +Data structures +-------------------- + +pytest internally maintains a dict of active funcargs with cache, param, +finalizer, (scopeitem?) information: + + active_funcargs = dict() + +if a parametrized "db" is activated: + + active_funcargs["db"] = FuncargInfo(dbvalue, paramindex, + FuncargFinalize(...), scopeitem) + +if a test is torn down and the next test requires a differently +parametrized "db": + + for argname in item.callspec.params: + if argname in active_funcargs: + funcarginfo = active_funcargs[argname] + if funcarginfo.param != item.callspec.params[argname]: + funcarginfo.callfinalizer() + del node2funcarg[funcarginfo.scopeitem] + del active_funcargs[argname] + nodes_to_be_torn_down = ... + for node in nodes_to_be_torn_down: + if node in node2funcarg: + argname = node2funcarg[node] + active_funcargs[argname].callfinalizer() + del node2funcarg[node] + del active_funcargs[argname] + +if a test is setup requiring a "db" funcarg: + + if "db" in active_funcargs: + return active_funcargs["db"][0] + funcarginfo = setup_funcarg() + active_funcargs["db"] = funcarginfo + node2funcarg[funcarginfo.scopeitem] = "db" Implementation plan for resources ------------------------------------------ diff -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a -r 07a765c1ea206c7271722f7e825e96c816c1d61a _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -465,13 +465,48 @@ marker = getattr(fac, "funcarg", None) if marker is not None: params = marker.kwargs.get("params") + scope = marker.kwargs.get("scope", "function") if params is not None: - metafunc.parametrize(argname, params, indirect=True) + metafunc.parametrize(argname, params, indirect=True, + scope=scope) newfuncargnames = getfuncargnames(fac) newfuncargnames.remove("request") funcargnames.extend(newfuncargnames) + def pytest_collection_modifyitems(self, items): + # separate parametrized setups + def sortparam(item1, item2): + try: + cs1 = item1.callspec + cs2 = item2.callspec + common = set(cs1.params).intersection(cs2.params) + except AttributeError: + pass + else: + if common: + common = list(common) + common.sort(key=lambda x: cs1._arg2scopenum[x]) + for x in common: + res = cmp(cs1.params[x], cs2.params[x]) + if res != 0: + return res + return 0 # leave previous order + items.sort(cmp=sortparam) + def pytest_runtest_teardown(self, item, nextitem): + try: + cs1 = item.callspec + except AttributeError: + return + for name in cs1.params: + try: + if name in nextitem.callspec.params and \ + cs1.params[name] == nextitem.callspec.params[name]: + continue + except AttributeError: + pass + key = (name, cs1.params[name]) + item.session._setupstate._callfinalizers(key) def _parsefactories(self, holderobj, nodeid): if holderobj in self._holderobjseen: diff -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a -r 07a765c1ea206c7271722f7e825e96c816c1d61a _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -499,11 +499,13 @@ self._globalid = _notexists self._globalid_args = set() self._globalparam = _notexists + self._arg2scopenum = {} # used for sorting parametrized resources def copy(self, metafunc): cs = CallSpec2(self.metafunc) cs.funcargs.update(self.funcargs) cs.params.update(self.params) + cs._arg2scopenum.update(self._arg2scopenum) cs._idlist = list(self._idlist) cs._globalid = self._globalid cs._globalid_args = self._globalid_args @@ -526,10 +528,11 @@ def id(self): return "-".join(map(str, filter(None, self._idlist))) - def setmulti(self, valtype, argnames, valset, id): + def setmulti(self, valtype, argnames, valset, id, scopenum=0): for arg,val in zip(argnames, valset): self._checkargnotcontained(arg) getattr(self, valtype)[arg] = val + self._arg2scopenum[arg] = scopenum self._idlist.append(id) def setall(self, funcargs, id, param): @@ -556,8 +559,10 @@ self.module = module self._calls = [] self._ids = py.builtin.set() + self._arg2scopenum = {} - def parametrize(self, argnames, argvalues, indirect=False, ids=None): + def parametrize(self, argnames, argvalues, indirect=False, ids=None, + scope="function"): """ Add new invocations to the underlying test function using the list of argvalues for the given argnames. Parametrization is performed during the collection phase. If you need to setup expensive resources @@ -581,6 +586,7 @@ if not isinstance(argnames, (tuple, list)): argnames = (argnames,) argvalues = [(val,) for val in argvalues] + scopenum = scopes.index(scope) if not indirect: #XXX should we also check for the opposite case? for arg in argnames: @@ -595,7 +601,8 @@ for i, valset in enumerate(argvalues): assert len(valset) == len(argnames) newcallspec = callspec.copy(self) - newcallspec.setmulti(valtype, argnames, valset, ids[i]) + newcallspec.setmulti(valtype, argnames, valset, ids[i], + scopenum) newcalls.append(newcallspec) self._calls = newcalls @@ -995,6 +1002,7 @@ def _callsetup(self): setuplist, allnames = self.funcargmanager.getsetuplist( self._pyfuncitem.nodeid) + mp = monkeypatch() for setupfunc, funcargnames in setuplist: kwargs = {} for name in funcargnames: @@ -1002,11 +1010,16 @@ kwargs[name] = self else: kwargs[name] = self.getfuncargvalue(name) + scope = readscope(setupfunc, "setup") - if scope is None: - setupfunc(**kwargs) - else: - self.cached_setup(lambda: setupfunc(**kwargs), scope=scope) + mp.setattr(self, 'scope', scope) + try: + if scope is None: + setupfunc(**kwargs) + else: + self.cached_setup(lambda: setupfunc(**kwargs), scope=scope) + finally: + mp.undo() def getfuncargvalue(self, argname): """ Retrieve a function argument by name for this test @@ -1047,7 +1060,7 @@ else: mp.setattr(self, 'param', param, raising=False) - # implemenet funcarg marker scope + # implement funcarg marker scope scope = readscope(funcargfactory, "funcarg") if scope is not None: @@ -1100,7 +1113,12 @@ self._addfinalizer(finalizer, scope=self.scope) def _addfinalizer(self, finalizer, scope): - colitem = self._getscopeitem(scope) + if scope != "function" and hasattr(self, "param"): + # parametrized resources are sorted by param + # so we rather store finalizers per (argname, param) + colitem = (self._currentarg, self.param) + else: + colitem = self._getscopeitem(scope) self._pyfuncitem.session._setupstate.addfinalizer( finalizer=finalizer, colitem=colitem) diff -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a -r 07a765c1ea206c7271722f7e825e96c816c1d61a _pytest/runner.py --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -294,6 +294,8 @@ """ attach a finalizer to the given colitem. if colitem is None, this will add a finalizer that is called at the end of teardown_all(). + if colitem is a tuple, it will be used as a key + and needs an explicit call to _callfinalizers(key) later on. """ assert hasattr(finalizer, '__call__') #assert colitem in self.stack @@ -311,15 +313,17 @@ def _teardown_with_finalization(self, colitem): self._callfinalizers(colitem) - if colitem: + if hasattr(colitem, "teardown"): colitem.teardown() for colitem in self._finalizers: - assert colitem is None or colitem in self.stack + assert colitem is None or colitem in self.stack \ + or isinstance(colitem, tuple) def teardown_all(self): while self.stack: self._pop_and_teardown() - self._teardown_with_finalization(None) + for key in list(self._finalizers): + self._teardown_with_finalization(key) assert not self._finalizers def teardown_exact(self, item, nextitem): diff -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a -r 07a765c1ea206c7271722f7e825e96c816c1d61a doc/en/example/newexamples.txt --- a/doc/en/example/newexamples.txt +++ b/doc/en/example/newexamples.txt @@ -48,7 +48,7 @@ ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -60,7 +60,7 @@ test_module.py:5: AssertionError ________________________________ test_noop _________________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -69,7 +69,7 @@ E assert 0 test_module.py:10: AssertionError - 2 failed in 0.27 seconds + 2 failed in 0.21 seconds you will see the two ``assert 0`` failing and can see that the same (session-scoped) object was passed into the two test functions. @@ -98,9 +98,31 @@ collecting ... collected 4 items FFFF ================================= FAILURES ================================= + ________________________ test_ehlo[mail.python.org] ________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + > assert "merlinux" in response[1] + E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + + test_module.py:4: AssertionError + ________________________ test_noop[mail.python.org] ________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError __________________________ test_ehlo[merlinux.eu] __________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -110,20 +132,9 @@ E assert 0 test_module.py:5: AssertionError - ________________________ test_ehlo[mail.python.org] ________________________ - - smtp = - - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - > assert "merlinux" in response[1] - E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' - - test_module.py:4: AssertionError __________________________ test_noop[merlinux.eu] __________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -132,20 +143,7 @@ E assert 0 test_module.py:10: AssertionError - ________________________ test_noop[mail.python.org] ________________________ - - smtp = - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - > assert 0 # for demo purposes - E assert 0 - - test_module.py:10: AssertionError - 4 failed in 6.91 seconds - closing - closing + 4 failed in 6.48 seconds We get four failures because we are running the two tests twice with different ``smtp`` instantiations as defined on the factory. @@ -157,14 +155,14 @@ $ py.test --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev5 plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 4 items + + - - ============================= in 0.02 seconds ============================= @@ -174,13 +172,13 @@ collecting ... collected 4 items FFFF ================================= FAILURES ================================= - /home/hpk/tmp/doc-exec-361/test_module.py:5: assert 0 - /home/hpk/tmp/doc-exec-361/test_module.py:4: assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' - /home/hpk/tmp/doc-exec-361/test_module.py:10: assert 0 - /home/hpk/tmp/doc-exec-361/test_module.py:10: assert 0 - 4 failed in 6.83 seconds - closing - closing + /home/hpk/tmp/doc-exec-386/test_module.py:4: assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + /home/hpk/tmp/doc-exec-386/test_module.py:10: assert 0 + /home/hpk/tmp/doc-exec-386/test_module.py:5: assert 0 + /home/hpk/tmp/doc-exec-386/test_module.py:10: assert 0 + 4 failed in 6.45 seconds + closing + closing .. _`new_setup`: @@ -217,8 +215,9 @@ # content of test_module.py import pytest - @pytest.mark.setup(scope="function") + @pytest.mark.setup(scope="module") def setresource(resource): + print "setupresource", resource global myresource myresource = resource @@ -236,10 +235,11 @@ collecting ... collected 2 items .. 2 passed in 0.24 seconds - created resource /home/hpk/tmp/pytest-3715/test_10 - using myresource /home/hpk/tmp/pytest-3715/test_10 - using myresource /home/hpk/tmp/pytest-3715/test_10 - finalize /home/hpk/tmp/pytest-3715/test_10 + created resource /home/hpk/tmp/pytest-3875/test_10 + setupresource /home/hpk/tmp/pytest-3875/test_10 + using myresource /home/hpk/tmp/pytest-3875/test_10 + using myresource /home/hpk/tmp/pytest-3875/test_10 + finalize /home/hpk/tmp/pytest-3875/test_10 The two test functions will see the same resource instance because it has a module life cycle or scope. @@ -264,23 +264,22 @@ $ py.test -qs collecting ... collected 4 items .... - 4 passed in 0.24 seconds - created resource /home/hpk/tmp/pytest-3716/test_1_aaa_0/aaa - using myresource /home/hpk/tmp/pytest-3716/test_1_aaa_0/aaa - created resource /home/hpk/tmp/pytest-3716/test_1_bbb_0/bbb - using myresource /home/hpk/tmp/pytest-3716/test_1_bbb_0/bbb - using myresource /home/hpk/tmp/pytest-3716/test_1_aaa_0/aaa - using myresource /home/hpk/tmp/pytest-3716/test_1_bbb_0/bbb - finalize /home/hpk/tmp/pytest-3716/test_1_bbb_0/bbb - finalize /home/hpk/tmp/pytest-3716/test_1_aaa_0/aaa + 4 passed in 0.25 seconds + created resource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa + setupresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa + using myresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa + using myresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa + finalize /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa + created resource /home/hpk/tmp/pytest-3876/test_1_bbb_0/bbb + using myresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa + using myresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa + finalize /home/hpk/tmp/pytest-3876/test_1_bbb_0/bbb Each parameter causes the creation of a respective resource and the unchanged test module uses it in its ``@setup`` decorated method. .. note:: - Currently, parametrized tests are sorted by test function location - so a test function will execute multiple times with different parametrized - funcargs. If you have class/module/session scoped funcargs and - they cause global side effects this can cause problems because the - code under test may not be prepared to deal with it. + Parametrized Resources will be grouped together during test execution. + Moreover, any added finalizers will be run before the next parametrized + resource is being setup. diff -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a -r 07a765c1ea206c7271722f7e825e96c816c1d61a doc/en/resources.txt --- a/doc/en/resources.txt +++ b/doc/en/resources.txt @@ -51,6 +51,8 @@ troubles than the current @setup approach which can share a lot of logic with the @funcarg one. +* tests are grouped by any parametrized resource + .. currentmodule:: _pytest @@ -252,58 +254,82 @@ overview of resource management in your project. -Sorting tests by funcarg scopes -------------------------------------------- +Grouping tests by resource parameters +---------------------------------------------------------- -.. note:: Not implemented, Under consideration. +.. note:: Implemented. -pytest by default sorts test items by their source location. -For class/module/session scoped funcargs it is not always -desirable to have multiple active funcargs. Sometimes, -the application under test may not even be able to handle it -because it relies on global state/side effects related to those -resources. +pytest usually sorts test items by their source location. +With pytest-2.X tests are first grouped by resource parameters. +If you have a parametrized resource, then all the tests using it +will first execute with it. Then any finalizers are called and then +the next parametrized resource instance is created and its tests are run. +Among other things, this allows to have per-session parametrized setups +including ones which affect global state of an application. -Therefore, pytest-2.3 tries to minimize the number of active -resources and re-orders test items accordingly. Consider the following -example:: +The following example uses two parametrized funcargs, one of which is +scoped on a per-module basis:: - @pytest.mark.funcarg(scope="module", params=[1,2]) - def arg(request): - ... + # content of test_module.py + import pytest + + @pytest.mark.funcarg(scope="module", params=["mod1", "mod2"]) + def modarg(request): + param = request.param + print "create", param + def fin(): + print "fin", param + request.addfinalizer(fin) + return param + @pytest.mark.funcarg(scope="function", params=[1,2]) def otherarg(request): - ... + return request.param def test_0(otherarg): - pass - def test_1(arg): - pass - def test_2(arg, otherarg): - pass + print " test0", otherarg + def test_1(modarg): + print " test1", modarg + def test_2(otherarg, modarg): + print " test2", otherarg, modarg -if arg.1, arg.2, otherarg.1, otherarg.2 denote the respective -parametrized funcarg instances this will re-order test -execution like follows:: +If you run the tests in verbose mode and with looking at captured output:: - test_0(otherarg.1) - test_0(otherarg.2) - test_1(arg.1) - test_2(arg.1, otherarg.1) - test_2(arg.1, otherarg.2) - test_1(arg.2) - test_2(arg.2, otherarg.1) - test_2(arg.2, otherarg.2) + $ py.test -v -s + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev5 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-382/.cache + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + collecting ... collected 8 items + + test_module.py:16: test_0[1] PASSED + test_module.py:16: test_0[2] PASSED + test_module.py:18: test_1[mod1] PASSED + test_module.py:20: test_2[1-mod1] PASSED + test_module.py:20: test_2[2-mod1] PASSED + test_module.py:18: test_1[mod2] PASSED + test_module.py:20: test_2[1-mod2] PASSED + test_module.py:20: test_2[2-mod2] PASSED + + ========================= 8 passed in 0.03 seconds ========================= + test0 1 + test0 2 + create mod1 + test1 mod1 + test2 1 mod1 + test2 2 mod1 + fin mod1 + create mod2 + test1 mod2 + test2 1 mod2 + test2 2 mod2 + fin mod2 -Moreover, test_2(arg.1) will execute any registered teardowns for -the arg.1 resource after the test finished execution. +You can see that that the parametrized ``modarg`` resource lead to +a re-ordering of test execution. Moreover, the finalizer for the +"mod1" parametrized resource was executed before the "mod2" resource +was setup with a different parameter. .. note:: - XXX it's quite unclear at the moment how to implement. - If we have a 1000 tests requiring different sets of parametrized - resources with different scopes, how to re-order accordingly? - It even seems difficult to express the expectation in a - concise manner. - - + The current implementation is experimental. diff -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a -r 07a765c1ea206c7271722f7e825e96c816c1d61a setup.py --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.0.dev4', + version='2.3.0.dev5', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a -r 07a765c1ea206c7271722f7e825e96c816c1d61a testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1695,7 +1695,7 @@ """) result = testdir.runpytest("-v") assert result.ret == 1 - result.stdout.fnmatch_lines([ + result.stdout.fnmatch_lines_random([ "*test_function*basic*PASSED", "*test_function*advanced*FAILED", ]) @@ -1849,10 +1849,10 @@ pass def test_result(arg): - assert len(l) == 2 - assert l == [1,2] + assert len(l) == arg + assert l[:arg] == [1,2][:arg] """) - reprec = testdir.inline_run("-s") + reprec = testdir.inline_run("-v", "-s") reprec.assertoutcome(passed=4) class TestFuncargMarker: @@ -2013,3 +2013,170 @@ """) reprec = testdir.inline_run() reprec.assertoutcome(passed=4) + + def test_scope_mismatch(self, testdir): + testdir.makeconftest(""" + import pytest + @pytest.mark.funcarg(scope="function") + def arg(request): + pass + """) + testdir.makepyfile(""" + import pytest + @pytest.mark.funcarg(scope="session") + def arg(request, arg): + pass + def test_mismatch(arg): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ScopeMismatch*", + "*1 error*", + ]) + + def test_parametrize_separated_order(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.mark.funcarg(scope="module", params=[1, 2]) + def arg(request): + return request.param + + l = [] + def test_1(arg): + l.append(arg) + def test_2(arg): + l.append(arg) + def test_3(): + assert len(l) == 4 + assert l[0] == l[1] + assert l[2] == l[3] + + """) + reprec = testdir.inline_run("-v") + reprec.assertoutcome(passed=5) + + def test_parametrize_separated_order_higher_scope_first(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.mark.funcarg(scope="function", params=[1, 2]) + def arg(request): + param = request.param + request.addfinalizer(lambda: l.append("fin:%s" % param)) + l.append("create:%s" % param) + return request.param + + @pytest.mark.funcarg(scope="module", params=["mod1", "mod2"]) + def modarg(request): + param = request.param + request.addfinalizer(lambda: l.append("fin:%s" % param)) + l.append("create:%s" % param) + return request.param + + l = [] + def test_1(arg): + l.append("test1") + def test_2(modarg): + l.append("test2") + def test_3(arg, modarg): + l.append("test3") + def test_4(modarg, arg): + l.append("test4") + def test_5(): + assert len(l) == 12 * 3 + import pprint + pprint.pprint(l) + assert l == [ + 'create:1', 'test1', 'fin:1', + 'create:2', 'test1', 'fin:2', + 'create:mod1', 'test2', 'create:1', 'test3', 'fin:1', + 'create:1', 'test4', 'fin:1', 'create:2', 'test3', 'fin:2', + 'create:2', 'test4', 'fin:mod1', 'fin:2', + + 'create:mod2', 'test2', 'create:1', 'test3', 'fin:1', + 'create:1', 'test4', 'fin:1', 'create:2', 'test3', 'fin:2', + 'create:2', 'test4', 'fin:mod2', 'fin:2', + ] + + """) + reprec = testdir.inline_run("-v") + reprec.assertoutcome(passed=12+1) + + def test_parametrize_separated_lifecycle(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.mark.funcarg(scope="module", params=[1, 2]) + def arg(request): + x = request.param + request.addfinalizer(lambda: l.append("fin%s" % x)) + return request.param + + l = [] + def test_1(arg): + l.append(arg) + def test_2(arg): + l.append(arg) + def test_3(): + assert len(l) == 6 + assert l[0] == l[1] + assert l[2] == "fin1" + assert l[3] == l[4] + assert l[5] == "fin2" + + """) + reprec = testdir.inline_run("-v") + reprec.assertoutcome(passed=5) + + def test_parametrize_function_scoped_finalizers_called(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.mark.funcarg(scope="function", params=[1, 2]) + def arg(request): + x = request.param + request.addfinalizer(lambda: l.append("fin%s" % x)) + return request.param + + l = [] + def test_1(arg): + l.append(arg) + def test_2(arg): + l.append(arg) + def test_3(): + assert len(l) == 8 + assert l == [1, "fin1", 1, "fin1", 2, "fin2", 2, "fin2"] + """) + reprec = testdir.inline_run("-v") + reprec.assertoutcome(passed=5) + + def test_parametrize_setup_function(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.mark.funcarg(scope="module", params=[1, 2]) + def arg(request): + return request.param + + @pytest.mark.setup(scope="module") + def mysetup(request, arg): + request.addfinalizer(lambda: l.append("fin%s" % arg)) + l.append("setup%s" % arg) + + l = [] + def test_1(arg): + l.append(arg) + def test_2(arg): + l.append(arg) + def test_3(): + import pprint + pprint.pprint(l) + assert l == ["setup1", 1, 1, "fin1", + "setup2", 2, 2, "fin2",] + + """) + reprec = testdir.inline_run("-v") + reprec.assertoutcome(passed=5) + https://bitbucket.org/hpk42/pytest/changeset/25e7f16d5b74/ changeset: 25e7f16d5b74 user: hpk42 date: 2012-07-30 11:51:50 summary: introduce a SetupCall, holding meta information and setup calling state affected #: 3 files diff -r 07a765c1ea206c7271722f7e825e96c816c1d61a -r 25e7f16d5b742dd3086510849c7dbc4d26a68f69 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -448,7 +448,7 @@ def pytest_generate_tests(self, metafunc): funcargnames = list(metafunc.funcargnames) - setuplist, allargnames = self.getsetuplist(metafunc.parentid) + _, allargnames = self.getsetuplist(metafunc.parentid) #print "setuplist, allargnames", setuplist, allargnames funcargnames.extend(allargnames) seen = set() @@ -529,7 +529,9 @@ # no funcargs. check if we have a setup function. setup = getattr(obj, "setup", None) if setup is not None and isinstance(setup, MarkInfo): - self.setuplist.append((nodeid, obj)) + scope = setup.kwargs.get("scope") + sf = SetupCall(self, nodeid, obj, scope) + self.setuplist.append(sf) continue faclist = self.arg2facspec.setdefault(argname, []) faclist.append((nodeid, obj)) @@ -537,12 +539,10 @@ def getsetuplist(self, nodeid): l = [] allargnames = set() - for baseid, setup in self.setuplist: - #print "check", baseid, setup - if nodeid.startswith(baseid): - funcargnames = getfuncargnames(setup) - l.append((setup, funcargnames)) - allargnames.update(funcargnames) + for setupcall in self.setuplist: + if nodeid.startswith(setupcall.baseid): + l.append(setupcall) + allargnames.update(setupcall.funcargnames) return l, allargnames @@ -575,6 +575,33 @@ raise FuncargLookupError(function, msg) +class SetupCall: + """ a container/helper for managing calls to setup functions. """ + def __init__(self, funcargmanager, baseid, func, scope): + self.funcargmanager = funcargmanager + self.baseid = baseid + self.func = func + self.funcargnames = getfuncargnames(func) + self.scope = scope + self.active = False + self._finalizer = [] + + def execute(self, kwargs): + #assert not self.active + self.active = True + mp = monkeypatch() + #if "request" in kwargs: + # request = kwargs["request"] + # def addfinalizer(func): + # #scopeitem = request._getscopeitem(scope) + # self._finalizer.append(func) + # mp.setattr(request, "addfinalizer", addfinalizer) + try: + self.func(**kwargs) + finally: + mp.undo() + + class NoMatch(Exception): """ raised if matching cannot locate a matching names. """ @@ -853,3 +880,8 @@ if numdefaults: return argnames[startindex:-numdefaults] return argnames[startindex:] + +def readscope(func, markattr): + marker = getattr(func, markattr, None) + if marker is not None: + return marker.kwargs.get("scope") diff -r 07a765c1ea206c7271722f7e825e96c816c1d61a -r 25e7f16d5b742dd3086510849c7dbc4d26a68f69 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -3,7 +3,7 @@ import inspect import sys import pytest -from _pytest.main import getfslineno, getfuncargnames +from _pytest.main import getfslineno, getfuncargnames, readscope from _pytest.monkeypatch import monkeypatch import _pytest @@ -1003,21 +1003,21 @@ setuplist, allnames = self.funcargmanager.getsetuplist( self._pyfuncitem.nodeid) mp = monkeypatch() - for setupfunc, funcargnames in setuplist: + for setupcall in setuplist: kwargs = {} - for name in funcargnames: + for name in setupcall.funcargnames: if name == "request": kwargs[name] = self else: kwargs[name] = self.getfuncargvalue(name) - scope = readscope(setupfunc, "setup") - mp.setattr(self, 'scope', scope) + mp.setattr(self, 'scope', setupcall.scope) try: - if scope is None: - setupfunc(**kwargs) + if setupcall.scope is None: + setupcall.execute(kwargs) else: - self.cached_setup(lambda: setupfunc(**kwargs), scope=scope) + self.cached_setup(lambda: setupcall.execute(kwargs), + scope=setupcall.scope) finally: mp.undo() @@ -1140,7 +1140,3 @@ new_kwargs[name] = kwargs[name] return new_kwargs -def readscope(func, markattr): - marker = getattr(func, markattr, None) - if marker is not None: - return marker.kwargs.get("scope") diff -r 07a765c1ea206c7271722f7e825e96c816c1d61a -r 25e7f16d5b742dd3086510849c7dbc4d26a68f69 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1752,10 +1752,14 @@ testdir.makeconftest(""" import pytest @pytest.mark.setup - def perfunction(request): + def perfunction(request, tmpdir): + pass + + @pytest.mark.funcarg + def arg1(request, tmpdir): pass @pytest.mark.setup - def perfunction2(request): + def perfunction2(request, arg1): pass def pytest_funcarg__fm(request): @@ -1769,12 +1773,16 @@ def test_parsefactories_conftest(self, testdir): testdir.makepyfile(""" def test_check_setup(item, fm): - setuplist, allnames = fm.getsetuplist(item.nodeid) - assert len(setuplist) == 2 - assert setuplist[0][0].__name__ == "perfunction" - assert "request" in setuplist[0][1] - assert setuplist[1][0].__name__ == "perfunction2" - assert "request" in setuplist[1][1] + setupcalls, allnames = fm.getsetuplist(item.nodeid) + assert len(setupcalls) == 2 + assert setupcalls[0].func.__name__ == "perfunction" + assert "request" in setupcalls[0].funcargnames + assert "tmpdir" in setupcalls[0].funcargnames + assert setupcalls[1].func.__name__ == "perfunction2" + assert "request" in setupcalls[1].funcargnames + assert "arg1" in setupcalls[1].funcargnames + assert "tmpdir" not in setupcalls[1].funcargnames + #assert "tmpdir" in setupcalls[1].depfuncargs """) reprec = testdir.inline_run("-s") reprec.assertoutcome(passed=1) https://bitbucket.org/hpk42/pytest/changeset/74b8701ed575/ changeset: 74b8701ed575 user: hpk42 date: 2012-07-30 12:39:45 summary: introduce a funcargcall object, holding meta information affected #: 3 files diff -r 25e7f16d5b742dd3086510849c7dbc4d26a68f69 -r 74b8701ed575b9372f96169b006a4f3395ee7b2a _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -454,24 +454,18 @@ seen = set() while funcargnames: argname = funcargnames.pop(0) - if argname in seen: + if argname in seen or argname == "request": continue seen.add(argname) faclist = self.getfactorylist(argname, metafunc.parentid, metafunc.function, raising=False) if faclist is None: continue # will raise FuncargLookupError at setup time - for fac in faclist: - marker = getattr(fac, "funcarg", None) - if marker is not None: - params = marker.kwargs.get("params") - scope = marker.kwargs.get("scope", "function") - if params is not None: - metafunc.parametrize(argname, params, indirect=True, - scope=scope) - newfuncargnames = getfuncargnames(fac) - newfuncargnames.remove("request") - funcargnames.extend(newfuncargnames) + for facdef in faclist: + if facdef.params is not None: + metafunc.parametrize(argname, facdef.params, indirect=True, + scope=facdef.scope) + funcargnames.extend(facdef.funcargnames) def pytest_collection_modifyitems(self, items): # separate parametrized setups @@ -520,11 +514,18 @@ continue # funcarg factories either have a pytest_funcarg__ prefix # or are "funcarg" marked - if hasattr(obj, "funcarg"): + if not callable(obj): + continue + marker = getattr(obj, "funcarg", None) + if marker is not None and isinstance(marker, MarkInfo): assert not name.startswith(self._argprefix) argname = name + scope = marker.kwargs.get("scope") + params = marker.kwargs.get("params") elif name.startswith(self._argprefix): argname = name[len(self._argprefix):] + scope = None + params = None else: # no funcargs. check if we have a setup function. setup = getattr(obj, "setup", None) @@ -534,7 +535,9 @@ self.setuplist.append(sf) continue faclist = self.arg2facspec.setdefault(argname, []) - faclist.append((nodeid, obj)) + factorydef = FactoryDef(self, nodeid, argname, obj, scope, params) + faclist.append(factorydef) + ### check scope/params mismatch? def getsetuplist(self, nodeid): l = [] @@ -548,19 +551,19 @@ def getfactorylist(self, argname, nodeid, function, raising=True): try: - factorydef = self.arg2facspec[argname] + factorydeflist = self.arg2facspec[argname] except KeyError: if raising: self._raiselookupfailed(argname, function, nodeid) else: - return self._matchfactories(factorydef, nodeid) + return self._matchfactories(factorydeflist, nodeid) - def _matchfactories(self, factorydef, nodeid): + def _matchfactories(self, factorydeflist, nodeid): l = [] - for baseid, factory in factorydef: + for factorydef in factorydeflist: #print "check", basepath, nodeid - if nodeid.startswith(baseid): - l.append(factory) + if nodeid.startswith(factorydef.baseid): + l.append(factorydef) return l def _raiselookupfailed(self, argname, function, nodeid): @@ -575,6 +578,17 @@ raise FuncargLookupError(function, msg) +class FactoryDef: + """ A container for a factory definition. """ + def __init__(self, funcargmanager, baseid, argname, func, scope, params): + self.funcargmanager = funcargmanager + self.baseid = baseid + self.func = func + self.argname = argname + self.scope = scope + self.params = params + self.funcargnames = getfuncargnames(func) + class SetupCall: """ a container/helper for managing calls to setup functions. """ def __init__(self, funcargmanager, baseid, func, scope): diff -r 25e7f16d5b742dd3086510849c7dbc4d26a68f69 -r 74b8701ed575b9372f96169b006a4f3395ee7b2a _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -586,6 +586,8 @@ if not isinstance(argnames, (tuple, list)): argnames = (argnames,) argvalues = [(val,) for val in argvalues] + if scope is None: + scope = "function" scopenum = scopes.index(scope) if not indirect: #XXX should we also check for the opposite case? @@ -888,16 +890,15 @@ self._factorystack = [] def _getfaclist(self, argname): - faclist = self._name2factory.get(argname, None) - if faclist is None: - faclist = self.funcargmanager.getfactorylist(argname, - self.parentid, - self.function) - self._name2factory[argname] = faclist - elif not faclist: + facdeflist = self._name2factory.get(argname, None) + if facdeflist is None: + facdeflist = self.funcargmanager.getfactorylist( + argname, self.parentid, self.function) + self._name2factory[argname] = facdeflist + elif not facdeflist: self.funcargmanager._raiselookupfailed(argname, self.function, self.parentid) - return faclist + return facdeflist def raiseerror(self, msg): """ raise a FuncargLookupError with the given message. """ @@ -1033,18 +1034,19 @@ return self._funcargs[argname] except KeyError: pass - factorylist = self._getfaclist(argname) - funcargfactory = factorylist.pop() - self._factorystack.append(funcargfactory) + factorydeflist = self._getfaclist(argname) + factorydef = factorydeflist.pop() + self._factorystack.append(factorydef) try: - return self._getfuncargvalue(funcargfactory, argname) + return self._getfuncargvalue(factorydef) finally: self._factorystack.pop() - def _getfuncargvalue(self, funcargfactory, argname): + def _getfuncargvalue(self, factorydef): # collect funcargs from the factory - newnames = getfuncargnames(funcargfactory) + newnames = list(factorydef.funcargnames) newnames.remove("request") + argname = factorydef.argname factory_kwargs = {"request": self} def fillfactoryargs(): for newname in newnames: @@ -1060,15 +1062,15 @@ else: mp.setattr(self, 'param', param, raising=False) - # implement funcarg marker scope - scope = readscope(funcargfactory, "funcarg") - + scope = factorydef.scope + funcargfactory = factorydef.func if scope is not None: __tracebackhide__ = True if scopemismatch(self.scope, scope): # try to report something helpful lines = [] - for factory in self._factorystack: + for factorydef in self._factorystack: + factory = factorydef.func fs, lineno = getfslineno(factory) p = self._pyfuncitem.session.fspath.bestrelpath(fs) args = inspect.formatargspec(*inspect.getargspec(factory)) diff -r 25e7f16d5b742dd3086510849c7dbc4d26a68f69 -r 74b8701ed575b9372f96169b006a4f3395ee7b2a testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1723,7 +1723,7 @@ faclist = fm.getfactorylist(name, item.nodeid, item.obj) assert len(faclist) == 1 fac = faclist[0] - assert fac.__name__ == "pytest_funcarg__" + name + assert fac.func.__name__ == "pytest_funcarg__" + name """) reprec = testdir.inline_run("-s") reprec.assertoutcome(passed=1) @@ -1739,9 +1739,9 @@ faclist = fm.getfactorylist("hello", item.nodeid, item.obj) print faclist assert len(faclist) == 3 - assert faclist[0](item._request) == "conftest" - assert faclist[1](item._request) == "module" - assert faclist[2](item._request) == "class" + assert faclist[0].func(item._request) == "conftest" + assert faclist[1].func(item._request) == "module" + assert faclist[2].func(item._request) == "class" """) reprec = testdir.inline_run("-s") reprec.assertoutcome(passed=1) https://bitbucket.org/hpk42/pytest/changeset/8359c8ab8465/ changeset: 8359c8ab8465 user: hpk42 date: 2012-08-01 09:07:32 summary: - enhance ordering of tests using parametrized resources - introduce a refined way to perform finalization for setup functions which does not use cached_setup() anymore affected #: 8 files diff -r 74b8701ed575b9372f96169b006a4f3395ee7b2a -r 8359c8ab8465ccd75c6f1a77363a27e0623162d8 _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.0.dev5' +__version__ = '2.3.0.dev6' diff -r 74b8701ed575b9372f96169b006a4f3395ee7b2a -r 8359c8ab8465ccd75c6f1a77363a27e0623162d8 _pytest/impl --- a/_pytest/impl +++ b/_pytest/impl @@ -1,3 +1,29 @@ +Sorting per-resource +----------------------------- + +for any given set of items: + +- collect items per session-scoped parametrized funcarg +- re-order until items no parametrizations are mixed + + examples: + + test() + test1(s1) + test1(s2) + test2() + test3(s1) + test3(s2) + + gets sorted to: + + test() + test2() + test1(s1) + test3(s1) + test1(s2) + test3(s2) + the new @setup functions -------------------------------------- diff -r 74b8701ed575b9372f96169b006a4f3395ee7b2a -r 8359c8ab8465ccd75c6f1a77363a27e0623162d8 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -425,6 +425,7 @@ session.config.pluginmanager.register(self, "funcmanage") self._holderobjseen = set() self.setuplist = [] + self._arg2finish = {} ### XXX this hook should be called for historic events like pytest_configure ### so that we don't have to do the below pytest_collection hook @@ -469,23 +470,7 @@ def pytest_collection_modifyitems(self, items): # separate parametrized setups - def sortparam(item1, item2): - try: - cs1 = item1.callspec - cs2 = item2.callspec - common = set(cs1.params).intersection(cs2.params) - except AttributeError: - pass - else: - if common: - common = list(common) - common.sort(key=lambda x: cs1._arg2scopenum[x]) - for x in common: - res = cmp(cs1.params[x], cs2.params[x]) - if res != 0: - return res - return 0 # leave previous order - items.sort(cmp=sortparam) + items[:] = parametrize_sorted(items, set(), {}, 0) def pytest_runtest_teardown(self, item, nextitem): try: @@ -501,6 +486,10 @@ pass key = (name, cs1.params[name]) item.session._setupstate._callfinalizers(key) + l = self._arg2finish.get(name) + if l is not None: + for fin in l: + fin() def _parsefactories(self, holderobj, nodeid): if holderobj in self._holderobjseen: @@ -577,17 +566,59 @@ msg += "\n use 'py.test --funcargs [testpath]' for help on them." raise FuncargLookupError(function, msg) + def ensure_setupcalls(self, request): + setuplist, allnames = self.getsetuplist(request._pyfuncitem.nodeid) + for setupcall in setuplist: + if setupcall.active: + continue + setuprequest = SetupRequest(request, setupcall) + kwargs = {} + for name in setupcall.funcargnames: + if name == "request": + kwargs[name] = setuprequest + else: + kwargs[name] = request.getfuncargvalue(name) + scope = setupcall.scope or "function" + scol = setupcall.scopeitem = request._getscopeitem(scope) + self.session._setupstate.addfinalizer(setupcall.finish, scol) + for argname in setupcall.funcargnames: # XXX all deps? + self.addargfinalizer(setupcall.finish, argname) + setupcall.execute(kwargs) -class FactoryDef: - """ A container for a factory definition. """ - def __init__(self, funcargmanager, baseid, argname, func, scope, params): - self.funcargmanager = funcargmanager - self.baseid = baseid - self.func = func - self.argname = argname - self.scope = scope - self.params = params - self.funcargnames = getfuncargnames(func) + def addargfinalizer(self, finalizer, argname): + l = self._arg2finish.setdefault(argname, []) + l.append(finalizer) + + def removefinalizer(self, finalizer): + for l in self._arg2finish.values(): + try: + l.remove(finalizer) + except ValueError: + pass + +def rprop(attr, doc=None): + if doc is None: + doc = "%r of underlying test item" + return property(lambda x: getattr(x._request, attr), doc=doc) + +class SetupRequest: + def __init__(self, request, setupcall): + self._request = request + self._setupcall = setupcall + self._finalizers = [] + + # no getfuncargvalue(), cached_setup, applymarker helpers here + # on purpose + + function = rprop("function") + cls = rprop("cls") + instance = rprop("instance") + fspath = rprop("fspath") + keywords = rprop("keywords") + config = rprop("config", "pytest config object.") + + def addfinalizer(self, finalizer): + self._setupcall.addfinalizer(finalizer) class SetupCall: """ a container/helper for managing calls to setup functions. """ @@ -601,20 +632,32 @@ self._finalizer = [] def execute(self, kwargs): - #assert not self.active + assert not self.active self.active = True - mp = monkeypatch() - #if "request" in kwargs: - # request = kwargs["request"] - # def addfinalizer(func): - # #scopeitem = request._getscopeitem(scope) - # self._finalizer.append(func) - # mp.setattr(request, "addfinalizer", addfinalizer) - try: - self.func(**kwargs) - finally: - mp.undo() + self.func(**kwargs) + def addfinalizer(self, finalizer): + assert self.active + self._finalizer.append(finalizer) + + def finish(self): + while self._finalizer: + func = self._finalizer.pop() + func() + # check neccesity of next commented call + self.funcargmanager.removefinalizer(self.finish) + self.active = False + +class FactoryDef: + """ A container for a factory definition. """ + def __init__(self, funcargmanager, baseid, argname, func, scope, params): + self.funcargmanager = funcargmanager + self.baseid = baseid + self.func = func + self.argname = argname + self.scope = scope + self.params = params + self.funcargnames = getfuncargnames(func) class NoMatch(Exception): """ raised if matching cannot locate a matching names. """ @@ -899,3 +942,75 @@ marker = getattr(func, markattr, None) if marker is not None: return marker.kwargs.get("scope") + +# algorithm for sorting on a per-parametrized resource setup basis + +def parametrize_sorted(items, ignore, cache, scopenum): + if scopenum >= 3: + return items + newitems = [] + olditems = [] + slicing_argparam = None + for item in items: + argparamlist = getfuncargparams(item, ignore, scopenum, cache) + if slicing_argparam is None and argparamlist: + slicing_argparam = argparamlist[0] + slicing_index = len(olditems) + if slicing_argparam in argparamlist: + newitems.append(item) + else: + olditems.append(item) + if newitems: + newignore = ignore.copy() + newignore.add(slicing_argparam) + newitems = parametrize_sorted(newitems + olditems[slicing_index:], + newignore, cache, scopenum) + old1 = parametrize_sorted(olditems[:slicing_index], newignore, + cache, scopenum+1) + return old1 + newitems + else: + olditems = parametrize_sorted(olditems, ignore, cache, scopenum+1) + return olditems + newitems + +def getfuncargparams(item, ignore, scopenum, cache): + """ return list of (arg,param) tuple, sorted by broader scope first. """ + assert scopenum < 3 # function + try: + cs = item.callspec + except AttributeError: + return [] + if scopenum == 0: + argparams = [x for x in cs.params.items() if x not in ignore + and cs._arg2scopenum[x[0]] == scopenum] + elif scopenum == 1: # module + argparams = [] + for argname, param in cs.params.items(): + if cs._arg2scopenum[argname] == scopenum: + key = (argname, param, item.fspath) + if key in ignore: + continue + argparams.append(key) + elif scopenum == 2: # class + argparams = [] + for argname, param in cs.params.items(): + if cs._arg2scopenum[argname] == scopenum: + l = cache.setdefault(item.fspath, []) + try: + i = l.index(item.cls) + except ValueError: + i = len(l) + l.append(item.cls) + key = (argname, param, item.fspath, i) + if key in ignore: + continue + argparams.append(key) + #elif scopenum == 3: + # argparams = [] + # for argname, param in cs.params.items(): + # if cs._arg2scopenum[argname] == scopenum: + # key = (argname, param, getfslineno(item.obj)) + # if key in ignore: + # continue + # argparams.append(key) + return argparams + diff -r 74b8701ed575b9372f96169b006a4f3395ee7b2a -r 8359c8ab8465ccd75c6f1a77363a27e0623162d8 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1001,26 +1001,7 @@ def _callsetup(self): - setuplist, allnames = self.funcargmanager.getsetuplist( - self._pyfuncitem.nodeid) - mp = monkeypatch() - for setupcall in setuplist: - kwargs = {} - for name in setupcall.funcargnames: - if name == "request": - kwargs[name] = self - else: - kwargs[name] = self.getfuncargvalue(name) - - mp.setattr(self, 'scope', setupcall.scope) - try: - if setupcall.scope is None: - setupcall.execute(kwargs) - else: - self.cached_setup(lambda: setupcall.execute(kwargs), - scope=setupcall.scope) - finally: - mp.undo() + self.funcargmanager.ensure_setupcalls(self) def getfuncargvalue(self, argname): """ Retrieve a function argument by name for this test diff -r 74b8701ed575b9372f96169b006a4f3395ee7b2a -r 8359c8ab8465ccd75c6f1a77363a27e0623162d8 doc/en/example/newexamples.txt --- a/doc/en/example/newexamples.txt +++ b/doc/en/example/newexamples.txt @@ -48,7 +48,7 @@ ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -60,7 +60,7 @@ test_module.py:5: AssertionError ________________________________ test_noop _________________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -69,7 +69,7 @@ E assert 0 test_module.py:10: AssertionError - 2 failed in 0.21 seconds + 2 failed in 0.26 seconds you will see the two ``assert 0`` failing and can see that the same (session-scoped) object was passed into the two test functions. @@ -98,31 +98,9 @@ collecting ... collected 4 items FFFF ================================= FAILURES ================================= - ________________________ test_ehlo[mail.python.org] ________________________ - - smtp = - - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - > assert "merlinux" in response[1] - E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' - - test_module.py:4: AssertionError - ________________________ test_noop[mail.python.org] ________________________ - - smtp = - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - > assert 0 # for demo purposes - E assert 0 - - test_module.py:10: AssertionError __________________________ test_ehlo[merlinux.eu] __________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -134,7 +112,7 @@ test_module.py:5: AssertionError __________________________ test_noop[merlinux.eu] __________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -143,7 +121,29 @@ E assert 0 test_module.py:10: AssertionError - 4 failed in 6.48 seconds + ________________________ test_ehlo[mail.python.org] ________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + > assert "merlinux" in response[1] + E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + + test_module.py:4: AssertionError + ________________________ test_noop[mail.python.org] ________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + 4 failed in 6.94 seconds We get four failures because we are running the two tests twice with different ``smtp`` instantiations as defined on the factory. @@ -159,10 +159,10 @@ plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 4 items + + - - ============================= in 0.02 seconds ============================= @@ -172,13 +172,13 @@ collecting ... collected 4 items FFFF ================================= FAILURES ================================= - /home/hpk/tmp/doc-exec-386/test_module.py:4: assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' - /home/hpk/tmp/doc-exec-386/test_module.py:10: assert 0 - /home/hpk/tmp/doc-exec-386/test_module.py:5: assert 0 - /home/hpk/tmp/doc-exec-386/test_module.py:10: assert 0 - 4 failed in 6.45 seconds - closing - closing + /home/hpk/tmp/doc-exec-389/test_module.py:5: assert 0 + /home/hpk/tmp/doc-exec-389/test_module.py:10: assert 0 + /home/hpk/tmp/doc-exec-389/test_module.py:4: assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + /home/hpk/tmp/doc-exec-389/test_module.py:10: assert 0 + 4 failed in 9.99 seconds + closing + closing .. _`new_setup`: @@ -191,8 +191,11 @@ The ``@pytest.mark.setup`` marker allows +* to define setup-functions close to test code or in conftest.py files + or plugins. * to mark a function as a setup/fixture method; the function can itself - receive funcargs + receive funcargs and will execute multiple times if the funcargs + are parametrized * to set a scope which determines the level of caching and how often the setup function is going to be called. @@ -234,12 +237,12 @@ $ py.test -qs collecting ... collected 2 items .. - 2 passed in 0.24 seconds - created resource /home/hpk/tmp/pytest-3875/test_10 - setupresource /home/hpk/tmp/pytest-3875/test_10 - using myresource /home/hpk/tmp/pytest-3875/test_10 - using myresource /home/hpk/tmp/pytest-3875/test_10 - finalize /home/hpk/tmp/pytest-3875/test_10 + 2 passed in 0.62 seconds + created resource /home/hpk/tmp/pytest-4224/test_10 + setupresource /home/hpk/tmp/pytest-4224/test_10 + using myresource /home/hpk/tmp/pytest-4224/test_10 + using myresource /home/hpk/tmp/pytest-4224/test_10 + finalize /home/hpk/tmp/pytest-4224/test_10 The two test functions will see the same resource instance because it has a module life cycle or scope. @@ -265,15 +268,16 @@ collecting ... collected 4 items .... 4 passed in 0.25 seconds - created resource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa - setupresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa - using myresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa - using myresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa - finalize /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa - created resource /home/hpk/tmp/pytest-3876/test_1_bbb_0/bbb - using myresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa - using myresource /home/hpk/tmp/pytest-3876/test_1_aaa_0/aaa - finalize /home/hpk/tmp/pytest-3876/test_1_bbb_0/bbb + created resource /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa + setupresource /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa + using myresource /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa + using myresource /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa + finalize /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa + created resource /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb + setupresource /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb + using myresource /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb + using myresource /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb + finalize /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb Each parameter causes the creation of a respective resource and the unchanged test module uses it in its ``@setup`` decorated method. diff -r 74b8701ed575b9372f96169b006a4f3395ee7b2a -r 8359c8ab8465ccd75c6f1a77363a27e0623162d8 doc/en/resources.txt --- a/doc/en/resources.txt +++ b/doc/en/resources.txt @@ -51,7 +51,7 @@ troubles than the current @setup approach which can share a lot of logic with the @funcarg one. -* tests are grouped by any parametrized resource +* tests are grouped by parametrized funcargs .. currentmodule:: _pytest @@ -259,13 +259,13 @@ .. note:: Implemented. -pytest usually sorts test items by their source location. -With pytest-2.X tests are first grouped by resource parameters. -If you have a parametrized resource, then all the tests using it +pytest used to always sort test items by their source location. +With pytest-2.X tests are first grouped by funcarg parameters. +If you have a parametrized funcarg, then all the tests using it will first execute with it. Then any finalizers are called and then the next parametrized resource instance is created and its tests are run. -Among other things, this allows to have per-session parametrized setups -including ones which affect global state of an application. +Among other things, this eases testing of applications which create +and use global state. The following example uses two parametrized funcargs, one of which is scoped on a per-module basis:: @@ -293,12 +293,12 @@ def test_2(otherarg, modarg): print " test2", otherarg, modarg -If you run the tests in verbose mode and with looking at captured output:: +Let's run the tests in verbose mode and with looking at the print-output:: $ py.test -v -s =========================== test session starts ============================ platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev5 -- /home/hpk/venv/1/bin/python - cachedir: /home/hpk/tmp/doc-exec-382/.cache + cachedir: /home/hpk/tmp/doc-exec-388/.cache plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 8 items @@ -326,9 +326,8 @@ fin mod2 You can see that that the parametrized ``modarg`` resource lead to -a re-ordering of test execution. Moreover, the finalizer for the -"mod1" parametrized resource was executed before the "mod2" resource -was setup with a different parameter. +a re-ordering of test execution. The finalizer for the "mod1" parametrized +resource was executed before the "mod2" resource was setup. .. note:: diff -r 74b8701ed575b9372f96169b006a4f3395ee7b2a -r 8359c8ab8465ccd75c6f1a77363a27e0623162d8 setup.py --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.0.dev5', + version='2.3.0.dev6', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r 74b8701ed575b9372f96169b006a4f3395ee7b2a -r 8359c8ab8465ccd75c6f1a77363a27e0623162d8 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1559,33 +1559,8 @@ ]) class TestRequestAPI: - @pytest.mark.xfail(reason="reverted refactoring") - 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*", - ]) - - @pytest.mark.xfail(reason="consider item's funcarg access and error conditions") - def test_runtest_setup_sees_filled_funcargs(self, testdir): + @pytest.mark.xfail(reason="consider flub feedback") + def test_setup_can_query_funcargs(self, testdir): testdir.makeconftest(""" def pytest_runtest_setup(item): assert not hasattr(item, "_request") @@ -1606,9 +1581,9 @@ result = testdir.makeconftest(""" import pytest - @pytest.mark.trylast - def pytest_runtest_setup(item): - assert item.funcargs == {"a": 1, "b": 2} + @pytest.mark.setup + def mysetup(testcontext): + testcontext.uses_funcarg("db") """) result = testdir.runpytest() assert result.ret == 0 @@ -1737,7 +1712,7 @@ return "class" def test_hello(self, item, fm): faclist = fm.getfactorylist("hello", item.nodeid, item.obj) - print faclist + print (faclist) assert len(faclist) == 3 assert faclist[0].func(item._request) == "conftest" assert faclist[1].func(item._request) == "module" @@ -1863,6 +1838,43 @@ reprec = testdir.inline_run("-v", "-s") reprec.assertoutcome(passed=4) + def test_class_function_parametrization_finalization(self, testdir): + p = testdir.makeconftest(""" + import pytest + import pprint + + l = [] + + @pytest.mark.funcarg(scope="function", params=[1,2]) + def farg(request): + return request.param + + @pytest.mark.funcarg(scope="class", params=list("ab")) + def carg(request): + return request.param + + @pytest.mark.setup(scope="class") + def append(request, farg, carg): + def fin(): + l.append("fin_%s%s" % (carg, farg)) + request.addfinalizer(fin) + """) + testdir.makepyfile(""" + import pytest + + class TestClass: + def test_1(self): + pass + class TestClass2: + def test_2(self): + pass + """) + reprec = testdir.inline_run("-v",) + reprec.assertoutcome(passed=8) + config = reprec.getcalls("pytest_unconfigure")[0].config + l = config._conftest.getconftestmodules(p)[0].l + assert l == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 + class TestFuncargMarker: def test_parametrize(self, testdir): testdir.makepyfile(""" @@ -2016,11 +2028,14 @@ l = [] def test_param(arg): l.append(arg) - def test_result(): - assert l == list("abc") """) - reprec = testdir.inline_run() - reprec.assertoutcome(passed=4) + reprec = testdir.inline_run("-v") + reprec.assertoutcome(passed=3) + l = reprec.getcalls("pytest_runtest_call")[0].item.module.l + assert len(l) == 3 + assert "a" in l + assert "b" in l + assert "c" in l def test_scope_mismatch(self, testdir): testdir.makeconftest(""" @@ -2056,14 +2071,105 @@ l.append(arg) def test_2(arg): l.append(arg) - def test_3(): - assert len(l) == 4 - assert l[0] == l[1] - assert l[2] == l[3] - """) reprec = testdir.inline_run("-v") - reprec.assertoutcome(passed=5) + reprec.assertoutcome(passed=4) + l = reprec.getcalls("pytest_runtest_call")[0].item.module.l + assert l == [1,1,2,2] + + def test_module_parametrized_ordering(self, testdir): + testdir.makeconftest(""" + import pytest + + @pytest.mark.funcarg(scope="session", params="s1 s2".split()) + def sarg(request): + pass + @pytest.mark.funcarg(scope="module", params="m1 m2".split()) + def marg(request): + pass + """) + testdir.makepyfile(test_mod1=""" + def test_func(sarg): + pass + def test_func1(marg): + pass + """, test_mod2=""" + def test_func2(sarg): + pass + def test_func3(sarg, marg): + pass + def test_func3b(sarg, marg): + pass + def test_func4(marg): + pass + """) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines(""" + test_mod1.py:1: test_func[s1] PASSED + test_mod2.py:1: test_func2[s1] PASSED + test_mod2.py:3: test_func3[s1-m1] PASSED + test_mod2.py:5: test_func3b[s1-m1] PASSED + test_mod2.py:3: test_func3[s1-m2] PASSED + test_mod2.py:5: test_func3b[s1-m2] PASSED + test_mod1.py:1: test_func[s2] PASSED + test_mod2.py:1: test_func2[s2] PASSED + test_mod2.py:3: test_func3[s2-m1] PASSED + test_mod2.py:5: test_func3b[s2-m1] PASSED + test_mod2.py:7: test_func4[m1] PASSED + test_mod2.py:3: test_func3[s2-m2] PASSED + test_mod2.py:5: test_func3b[s2-m2] PASSED + test_mod2.py:7: test_func4[m2] PASSED + test_mod1.py:3: test_func1[m1] PASSED + test_mod1.py:3: test_func1[m2] PASSED + """) + + def test_class_ordering(self, testdir): + p = testdir.makeconftest(""" + import pytest + + l = [] + + @pytest.mark.funcarg(scope="function", params=[1,2]) + def farg(request): + return request.param + + @pytest.mark.funcarg(scope="class", params=list("ab")) + def carg(request): + return request.param + + @pytest.mark.setup(scope="class") + def append(request, farg, carg): + def fin(): + l.append("fin_%s%s" % (carg, farg)) + request.addfinalizer(fin) + """) + testdir.makepyfile(""" + import pytest + + class TestClass2: + def test_1(self): + pass + def test_2(self): + pass + class TestClass: + def test_3(self): + pass + """) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines(""" + test_class_ordering.py:4: TestClass2.test_1[1-a] PASSED + test_class_ordering.py:4: TestClass2.test_1[2-a] PASSED + test_class_ordering.py:6: TestClass2.test_2[1-a] PASSED + test_class_ordering.py:6: TestClass2.test_2[2-a] PASSED + test_class_ordering.py:4: TestClass2.test_1[1-b] PASSED + test_class_ordering.py:4: TestClass2.test_1[2-b] PASSED + test_class_ordering.py:6: TestClass2.test_2[1-b] PASSED + test_class_ordering.py:6: TestClass2.test_2[2-b] PASSED + test_class_ordering.py:9: TestClass.test_3[1-a] PASSED + test_class_ordering.py:9: TestClass.test_3[2-a] PASSED + test_class_ordering.py:9: TestClass.test_3[1-b] PASSED + test_class_ordering.py:9: TestClass.test_3[2-b] PASSED + """) def test_parametrize_separated_order_higher_scope_first(self, testdir): testdir.makepyfile(""" @@ -2097,17 +2203,14 @@ import pprint pprint.pprint(l) assert l == [ - 'create:1', 'test1', 'fin:1', - 'create:2', 'test1', 'fin:2', - 'create:mod1', 'test2', 'create:1', 'test3', 'fin:1', - 'create:1', 'test4', 'fin:1', 'create:2', 'test3', 'fin:2', - 'create:2', 'test4', 'fin:mod1', 'fin:2', - - 'create:mod2', 'test2', 'create:1', 'test3', 'fin:1', - 'create:1', 'test4', 'fin:1', 'create:2', 'test3', 'fin:2', - 'create:2', 'test4', 'fin:mod2', 'fin:2', - ] - + 'create:1', 'test1', 'fin:1', 'create:2', 'test1', + 'fin:2', 'create:mod1', 'test2', 'create:1', 'test3', + 'fin:1', 'create:2', 'test3', 'fin:2', 'create:1', + 'test4', 'fin:1', 'create:2', 'test4', 'fin:mod1', + 'fin:2', 'create:mod2', 'test2', 'create:1', 'test3', + 'fin:1', 'create:2', 'test3', 'fin:2', 'create:1', + 'test4', 'fin:1', 'create:2', 'test4', 'fin:mod2', + 'fin:2'] """) reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=12+1) @@ -2118,6 +2221,7 @@ @pytest.mark.funcarg(scope="module", params=[1, 2]) def arg(request): + request.config.l = l # to access from outer x = request.param request.addfinalizer(lambda: l.append("fin%s" % x)) return request.param @@ -2127,16 +2231,18 @@ l.append(arg) def test_2(arg): l.append(arg) - def test_3(): - assert len(l) == 6 - assert l[0] == l[1] - assert l[2] == "fin1" - assert l[3] == l[4] - assert l[5] == "fin2" - """) reprec = testdir.inline_run("-v") - reprec.assertoutcome(passed=5) + reprec.assertoutcome(passed=4) + l = reprec.getcalls("pytest_configure")[0].config.l + import pprint + pprint.pprint(l) + assert len(l) == 6 + assert l[0] == l[1] == 1 + assert l[2] == "fin1" + assert l[3] == l[4] == 2 + assert l[5] == "fin2" + def test_parametrize_function_scoped_finalizers_called(self, testdir): testdir.makepyfile(""" @@ -2155,7 +2261,7 @@ l.append(arg) def test_3(): assert len(l) == 8 - assert l == [1, "fin1", 1, "fin1", 2, "fin2", 2, "fin2"] + assert l == [1, "fin1", 2, "fin2", 1, "fin1", 2, "fin2"] """) reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=5) @@ -2181,10 +2287,13 @@ def test_3(): import pprint pprint.pprint(l) - assert l == ["setup1", 1, 1, "fin1", - "setup2", 2, 2, "fin2",] + if arg == 1: + assert l == ["setup1", 1, 1, ] + elif arg == 2: + assert l == ["setup1", 1, 1, "fin1", + "setup2", 2, 2, ] """) reprec = testdir.inline_run("-v") - reprec.assertoutcome(passed=5) + reprec.assertoutcome(passed=6) Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Aug 1 09:10:49 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 01 Aug 2012 07:10:49 -0000 Subject: [py-svn] commit/pytest: hpk42: mark a test as xfailing on python2.5 Message-ID: <20120801071049.20510.36174@bitbucket15.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/6b2daddff4f5/ changeset: 6b2daddff4f5 user: hpk42 date: 2012-08-01 09:10:40 summary: mark a test as xfailing on python2.5 affected #: 1 file diff -r 8359c8ab8465ccd75c6f1a77363a27e0623162d8 -r 6b2daddff4f58e4b2d2bfe64ab850d6ec3c77d9e testing/test_assertion.py --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -151,6 +151,7 @@ ]) + at pytest.mark.xfail("sys.version_info < (2,6)") def test_assert_compare_truncate_longmessage(testdir): testdir.makepyfile(r""" def test_long(): Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Aug 1 14:53:49 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 01 Aug 2012 12:53:49 -0000 Subject: [py-svn] commit/pytest: 3 new changesets Message-ID: <20120801125349.24710.24052@bitbucket12.managed.contegix.com> 3 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/a34510e74c40/ changeset: a34510e74c40 user: hpk42 date: 2012-08-01 09:23:39 summary: reorder internal layout so that funcarg-related functionality is in python.py affected #: 3 files diff -r 6b2daddff4f58e4b2d2bfe64ab850d6ec3c77d9e -r a34510e74c40c5ddd12745ff2760dd0571e879a8 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -5,9 +5,6 @@ import inspect import os, sys, imp -from _pytest.monkeypatch import monkeypatch -from py._code.code import TerminalRepr - from _pytest.mark import MarkInfo tracebackcutdir = py.path.local(_pytest.__file__).dirpath() @@ -285,15 +282,15 @@ pass def _repr_failure_py(self, excinfo, style=None): - LE = self.session.funcargmanager.FuncargLookupError - if excinfo.errisinstance(LE): + fm = self.session.funcargmanager + if excinfo.errisinstance(fm.FuncargLookupError): function = excinfo.value.function if function is not None: fspath, lineno = getfslineno(function) lines, _ = inspect.getsourcelines(function) for i, line in enumerate(lines): if line.strip().startswith('def'): - return FuncargLookupErrorRepr(fspath, + return fm.FuncargLookupErrorRepr(fspath, lineno, lines[:i+1], str(excinfo.value.msg)) if self.config.option.fulltrace: @@ -408,257 +405,6 @@ self._location = location return location -class FuncargLookupError(LookupError): - """ could not find a factory. """ - def __init__(self, function, msg): - self.function = function - self.msg = msg - -class FuncargManager: - _argprefix = "pytest_funcarg__" - FuncargLookupError = FuncargLookupError - - def __init__(self, session): - self.session = session - self.config = session.config - self.arg2facspec = {} - session.config.pluginmanager.register(self, "funcmanage") - self._holderobjseen = set() - self.setuplist = [] - self._arg2finish = {} - - ### XXX this hook should be called for historic events like pytest_configure - ### so that we don't have to do the below pytest_collection hook - def pytest_plugin_registered(self, plugin): - #print "plugin_registered", plugin - nodeid = "" - try: - p = py.path.local(plugin.__file__) - except AttributeError: - pass - else: - if p.basename.startswith("conftest.py"): - nodeid = p.dirpath().relto(self.session.fspath) - self._parsefactories(plugin, nodeid) - - @pytest.mark.tryfirst - def pytest_collection(self, session): - plugins = session.config.pluginmanager.getplugins() - for plugin in plugins: - self.pytest_plugin_registered(plugin) - - def pytest_generate_tests(self, metafunc): - funcargnames = list(metafunc.funcargnames) - _, allargnames = self.getsetuplist(metafunc.parentid) - #print "setuplist, allargnames", setuplist, allargnames - funcargnames.extend(allargnames) - seen = set() - while funcargnames: - argname = funcargnames.pop(0) - if argname in seen or argname == "request": - continue - seen.add(argname) - faclist = self.getfactorylist(argname, metafunc.parentid, - metafunc.function, raising=False) - if faclist is None: - continue # will raise FuncargLookupError at setup time - for facdef in faclist: - if facdef.params is not None: - metafunc.parametrize(argname, facdef.params, indirect=True, - scope=facdef.scope) - funcargnames.extend(facdef.funcargnames) - - def pytest_collection_modifyitems(self, items): - # separate parametrized setups - items[:] = parametrize_sorted(items, set(), {}, 0) - - def pytest_runtest_teardown(self, item, nextitem): - try: - cs1 = item.callspec - except AttributeError: - return - for name in cs1.params: - try: - if name in nextitem.callspec.params and \ - cs1.params[name] == nextitem.callspec.params[name]: - continue - except AttributeError: - pass - key = (name, cs1.params[name]) - item.session._setupstate._callfinalizers(key) - l = self._arg2finish.get(name) - if l is not None: - for fin in l: - fin() - - def _parsefactories(self, holderobj, nodeid): - if holderobj in self._holderobjseen: - return - #print "parsefactories", holderobj - self._holderobjseen.add(holderobj) - for name in dir(holderobj): - #print "check", holderobj, name - obj = getattr(holderobj, name) - if not callable(obj): - continue - # funcarg factories either have a pytest_funcarg__ prefix - # or are "funcarg" marked - if not callable(obj): - continue - marker = getattr(obj, "funcarg", None) - if marker is not None and isinstance(marker, MarkInfo): - assert not name.startswith(self._argprefix) - argname = name - scope = marker.kwargs.get("scope") - params = marker.kwargs.get("params") - elif name.startswith(self._argprefix): - argname = name[len(self._argprefix):] - scope = None - params = None - else: - # no funcargs. check if we have a setup function. - setup = getattr(obj, "setup", None) - if setup is not None and isinstance(setup, MarkInfo): - scope = setup.kwargs.get("scope") - sf = SetupCall(self, nodeid, obj, scope) - self.setuplist.append(sf) - continue - faclist = self.arg2facspec.setdefault(argname, []) - factorydef = FactoryDef(self, nodeid, argname, obj, scope, params) - faclist.append(factorydef) - ### check scope/params mismatch? - - def getsetuplist(self, nodeid): - l = [] - allargnames = set() - for setupcall in self.setuplist: - if nodeid.startswith(setupcall.baseid): - l.append(setupcall) - allargnames.update(setupcall.funcargnames) - return l, allargnames - - - def getfactorylist(self, argname, nodeid, function, raising=True): - try: - factorydeflist = self.arg2facspec[argname] - except KeyError: - if raising: - self._raiselookupfailed(argname, function, nodeid) - else: - return self._matchfactories(factorydeflist, nodeid) - - def _matchfactories(self, factorydeflist, nodeid): - l = [] - for factorydef in factorydeflist: - #print "check", basepath, nodeid - if nodeid.startswith(factorydef.baseid): - l.append(factorydef) - return l - - def _raiselookupfailed(self, argname, function, nodeid): - available = [] - for name, facdef in self.arg2facspec.items(): - faclist = self._matchfactories(facdef, nodeid) - if faclist: - available.append(name) - msg = "LookupError: no factory found for argument %r" % (argname,) - msg += "\n available funcargs: %s" %(", ".join(available),) - msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise FuncargLookupError(function, msg) - - def ensure_setupcalls(self, request): - setuplist, allnames = self.getsetuplist(request._pyfuncitem.nodeid) - for setupcall in setuplist: - if setupcall.active: - continue - setuprequest = SetupRequest(request, setupcall) - kwargs = {} - for name in setupcall.funcargnames: - if name == "request": - kwargs[name] = setuprequest - else: - kwargs[name] = request.getfuncargvalue(name) - scope = setupcall.scope or "function" - scol = setupcall.scopeitem = request._getscopeitem(scope) - self.session._setupstate.addfinalizer(setupcall.finish, scol) - for argname in setupcall.funcargnames: # XXX all deps? - self.addargfinalizer(setupcall.finish, argname) - setupcall.execute(kwargs) - - def addargfinalizer(self, finalizer, argname): - l = self._arg2finish.setdefault(argname, []) - l.append(finalizer) - - def removefinalizer(self, finalizer): - for l in self._arg2finish.values(): - try: - l.remove(finalizer) - except ValueError: - pass - -def rprop(attr, doc=None): - if doc is None: - doc = "%r of underlying test item" - return property(lambda x: getattr(x._request, attr), doc=doc) - -class SetupRequest: - def __init__(self, request, setupcall): - self._request = request - self._setupcall = setupcall - self._finalizers = [] - - # no getfuncargvalue(), cached_setup, applymarker helpers here - # on purpose - - function = rprop("function") - cls = rprop("cls") - instance = rprop("instance") - fspath = rprop("fspath") - keywords = rprop("keywords") - config = rprop("config", "pytest config object.") - - def addfinalizer(self, finalizer): - self._setupcall.addfinalizer(finalizer) - -class SetupCall: - """ a container/helper for managing calls to setup functions. """ - def __init__(self, funcargmanager, baseid, func, scope): - self.funcargmanager = funcargmanager - self.baseid = baseid - self.func = func - self.funcargnames = getfuncargnames(func) - self.scope = scope - self.active = False - self._finalizer = [] - - def execute(self, kwargs): - assert not self.active - self.active = True - self.func(**kwargs) - - def addfinalizer(self, finalizer): - assert self.active - self._finalizer.append(finalizer) - - def finish(self): - while self._finalizer: - func = self._finalizer.pop() - func() - # check neccesity of next commented call - self.funcargmanager.removefinalizer(self.finish) - self.active = False - -class FactoryDef: - """ A container for a factory definition. """ - def __init__(self, funcargmanager, baseid, argname, func, scope, params): - self.funcargmanager = funcargmanager - self.baseid = baseid - self.func = func - self.argname = argname - self.scope = scope - self.params = params - self.funcargnames = getfuncargnames(func) - class NoMatch(Exception): """ raised if matching cannot locate a matching names. """ @@ -675,7 +421,6 @@ self.shouldstop = False self.trace = config.trace.root.get("collection") self._norecursepatterns = config.getini("norecursedirs") - self.funcargmanager = FuncargManager(self) def pytest_collectstart(self): if self.shouldstop: @@ -879,28 +624,6 @@ yield x node.ihook.pytest_collectreport(report=rep) - # XXX not used yet - def register_resource_factory(self, name, factoryfunc, - matchscope=None, - cachescope=None): - """ register a factory function for the given name. - - :param name: the name which can be used to retrieve a value constructed - by the factory function later. - :param factoryfunc: a function accepting (name, reqnode) parameters - and returning a value. - :param matchscope: denotes visibility of the factory func. - Pass a particular Node instance if you want to - restrict factory function visilbility to its descendants. - Pass None if you want the factory func to be globally - availabile. - :param cachescope: denotes caching scope. If you pass a node instance - the value returned by getresource() will be reused - for all descendants of that node. Pass None (the default) - if you want no caching. Pass "session" if you want to - to cache on a per-session level. - """ - def getfslineno(obj): # xxx let decorators etc specify a sane ordering if hasattr(obj, 'place_as'): @@ -909,108 +632,3 @@ assert isinstance(fslineno[1], int), obj return fslineno - -class FuncargLookupErrorRepr(TerminalRepr): - def __init__(self, filename, firstlineno, deflines, errorstring): - self.deflines = deflines - self.errorstring = errorstring - self.filename = filename - self.firstlineno = firstlineno - - def toterminal(self, tw): - tw.line() - for line in self.deflines: - tw.line(" " + line.strip()) - for line in self.errorstring.split("\n"): - tw.line(" " + line.strip(), red=True) - tw.line() - tw.line("%s:%d" % (self.filename, self.firstlineno+1)) - -def getfuncargnames(function, startindex=None): - # XXX merge with main.py's varnames - argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] - if startindex is None: - startindex = py.std.inspect.ismethod(function) and 1 or 0 - defaults = getattr(function, 'func_defaults', - getattr(function, '__defaults__', None)) or () - numdefaults = len(defaults) - if numdefaults: - return argnames[startindex:-numdefaults] - return argnames[startindex:] - -def readscope(func, markattr): - marker = getattr(func, markattr, None) - if marker is not None: - return marker.kwargs.get("scope") - -# algorithm for sorting on a per-parametrized resource setup basis - -def parametrize_sorted(items, ignore, cache, scopenum): - if scopenum >= 3: - return items - newitems = [] - olditems = [] - slicing_argparam = None - for item in items: - argparamlist = getfuncargparams(item, ignore, scopenum, cache) - if slicing_argparam is None and argparamlist: - slicing_argparam = argparamlist[0] - slicing_index = len(olditems) - if slicing_argparam in argparamlist: - newitems.append(item) - else: - olditems.append(item) - if newitems: - newignore = ignore.copy() - newignore.add(slicing_argparam) - newitems = parametrize_sorted(newitems + olditems[slicing_index:], - newignore, cache, scopenum) - old1 = parametrize_sorted(olditems[:slicing_index], newignore, - cache, scopenum+1) - return old1 + newitems - else: - olditems = parametrize_sorted(olditems, ignore, cache, scopenum+1) - return olditems + newitems - -def getfuncargparams(item, ignore, scopenum, cache): - """ return list of (arg,param) tuple, sorted by broader scope first. """ - assert scopenum < 3 # function - try: - cs = item.callspec - except AttributeError: - return [] - if scopenum == 0: - argparams = [x for x in cs.params.items() if x not in ignore - and cs._arg2scopenum[x[0]] == scopenum] - elif scopenum == 1: # module - argparams = [] - for argname, param in cs.params.items(): - if cs._arg2scopenum[argname] == scopenum: - key = (argname, param, item.fspath) - if key in ignore: - continue - argparams.append(key) - elif scopenum == 2: # class - argparams = [] - for argname, param in cs.params.items(): - if cs._arg2scopenum[argname] == scopenum: - l = cache.setdefault(item.fspath, []) - try: - i = l.index(item.cls) - except ValueError: - i = len(l) - l.append(item.cls) - key = (argname, param, item.fspath, i) - if key in ignore: - continue - argparams.append(key) - #elif scopenum == 3: - # argparams = [] - # for argname, param in cs.params.items(): - # if cs._arg2scopenum[argname] == scopenum: - # key = (argname, param, getfslineno(item.obj)) - # if key in ignore: - # continue - # argparams.append(key) - return argparams - diff -r 6b2daddff4f58e4b2d2bfe64ab850d6ec3c77d9e -r a34510e74c40c5ddd12745ff2760dd0571e879a8 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -3,8 +3,10 @@ import inspect import sys import pytest -from _pytest.main import getfslineno, getfuncargnames, readscope +from _pytest.main import getfslineno from _pytest.monkeypatch import monkeypatch +from py._code.code import TerminalRepr +from _pytest.mark import MarkInfo import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() @@ -69,6 +71,8 @@ "test function, one with arg1=1 and another with arg1=2." ) +def pytest_sessionstart(session): + session.funcargmanager = FuncargManager(session) @pytest.mark.trylast def pytest_namespace(): @@ -1123,3 +1127,355 @@ new_kwargs[name] = kwargs[name] return new_kwargs +class FuncargLookupError(LookupError): + """ could not find a factory. """ + def __init__(self, function, msg): + self.function = function + self.msg = msg + +class FuncargLookupErrorRepr(TerminalRepr): + def __init__(self, filename, firstlineno, deflines, errorstring): + self.deflines = deflines + self.errorstring = errorstring + self.filename = filename + self.firstlineno = firstlineno + + def toterminal(self, tw): + tw.line() + for line in self.deflines: + tw.line(" " + line.strip()) + for line in self.errorstring.split("\n"): + tw.line(" " + line.strip(), red=True) + tw.line() + tw.line("%s:%d" % (self.filename, self.firstlineno+1)) + +class FuncargManager: + _argprefix = "pytest_funcarg__" + FuncargLookupError = FuncargLookupError + FuncargLookupErrorRepr = FuncargLookupErrorRepr + + def __init__(self, session): + self.session = session + self.config = session.config + self.arg2facspec = {} + session.config.pluginmanager.register(self, "funcmanage") + self._holderobjseen = set() + self.setuplist = [] + self._arg2finish = {} + + ### XXX this hook should be called for historic events like pytest_configure + ### so that we don't have to do the below pytest_collection hook + def pytest_plugin_registered(self, plugin): + #print "plugin_registered", plugin + nodeid = "" + try: + p = py.path.local(plugin.__file__) + except AttributeError: + pass + else: + if p.basename.startswith("conftest.py"): + nodeid = p.dirpath().relto(self.session.fspath) + self._parsefactories(plugin, nodeid) + + @pytest.mark.tryfirst + def pytest_collection(self, session): + plugins = session.config.pluginmanager.getplugins() + for plugin in plugins: + self.pytest_plugin_registered(plugin) + + def pytest_generate_tests(self, metafunc): + funcargnames = list(metafunc.funcargnames) + _, allargnames = self.getsetuplist(metafunc.parentid) + #print "setuplist, allargnames", setuplist, allargnames + funcargnames.extend(allargnames) + seen = set() + while funcargnames: + argname = funcargnames.pop(0) + if argname in seen or argname == "request": + continue + seen.add(argname) + faclist = self.getfactorylist(argname, metafunc.parentid, + metafunc.function, raising=False) + if faclist is None: + continue # will raise FuncargLookupError at setup time + for facdef in faclist: + if facdef.params is not None: + metafunc.parametrize(argname, facdef.params, indirect=True, + scope=facdef.scope) + funcargnames.extend(facdef.funcargnames) + + def pytest_collection_modifyitems(self, items): + # separate parametrized setups + items[:] = parametrize_sorted(items, set(), {}, 0) + + def pytest_runtest_teardown(self, item, nextitem): + try: + cs1 = item.callspec + except AttributeError: + return + for name in cs1.params: + try: + if name in nextitem.callspec.params and \ + cs1.params[name] == nextitem.callspec.params[name]: + continue + except AttributeError: + pass + key = (name, cs1.params[name]) + item.session._setupstate._callfinalizers(key) + l = self._arg2finish.get(name) + if l is not None: + for fin in l: + fin() + + def _parsefactories(self, holderobj, nodeid): + if holderobj in self._holderobjseen: + return + #print "parsefactories", holderobj + self._holderobjseen.add(holderobj) + for name in dir(holderobj): + #print "check", holderobj, name + obj = getattr(holderobj, name) + if not callable(obj): + continue + # funcarg factories either have a pytest_funcarg__ prefix + # or are "funcarg" marked + if not callable(obj): + continue + marker = getattr(obj, "funcarg", None) + if marker is not None and isinstance(marker, MarkInfo): + assert not name.startswith(self._argprefix) + argname = name + scope = marker.kwargs.get("scope") + params = marker.kwargs.get("params") + elif name.startswith(self._argprefix): + argname = name[len(self._argprefix):] + scope = None + params = None + else: + # no funcargs. check if we have a setup function. + setup = getattr(obj, "setup", None) + if setup is not None and isinstance(setup, MarkInfo): + scope = setup.kwargs.get("scope") + sf = SetupCall(self, nodeid, obj, scope) + self.setuplist.append(sf) + continue + faclist = self.arg2facspec.setdefault(argname, []) + factorydef = FactoryDef(self, nodeid, argname, obj, scope, params) + faclist.append(factorydef) + ### check scope/params mismatch? + + def getsetuplist(self, nodeid): + l = [] + allargnames = set() + for setupcall in self.setuplist: + if nodeid.startswith(setupcall.baseid): + l.append(setupcall) + allargnames.update(setupcall.funcargnames) + return l, allargnames + + + def getfactorylist(self, argname, nodeid, function, raising=True): + try: + factorydeflist = self.arg2facspec[argname] + except KeyError: + if raising: + self._raiselookupfailed(argname, function, nodeid) + else: + return self._matchfactories(factorydeflist, nodeid) + + def _matchfactories(self, factorydeflist, nodeid): + l = [] + for factorydef in factorydeflist: + #print "check", basepath, nodeid + if nodeid.startswith(factorydef.baseid): + l.append(factorydef) + return l + + def _raiselookupfailed(self, argname, function, nodeid): + available = [] + for name, facdef in self.arg2facspec.items(): + faclist = self._matchfactories(facdef, nodeid) + if faclist: + available.append(name) + msg = "LookupError: no factory found for argument %r" % (argname,) + msg += "\n available funcargs: %s" %(", ".join(available),) + msg += "\n use 'py.test --funcargs [testpath]' for help on them." + raise FuncargLookupError(function, msg) + + def ensure_setupcalls(self, request): + setuplist, allnames = self.getsetuplist(request._pyfuncitem.nodeid) + for setupcall in setuplist: + if setupcall.active: + continue + setuprequest = SetupRequest(request, setupcall) + kwargs = {} + for name in setupcall.funcargnames: + if name == "request": + kwargs[name] = setuprequest + else: + kwargs[name] = request.getfuncargvalue(name) + scope = setupcall.scope or "function" + scol = setupcall.scopeitem = request._getscopeitem(scope) + self.session._setupstate.addfinalizer(setupcall.finish, scol) + for argname in setupcall.funcargnames: # XXX all deps? + self.addargfinalizer(setupcall.finish, argname) + setupcall.execute(kwargs) + + def addargfinalizer(self, finalizer, argname): + l = self._arg2finish.setdefault(argname, []) + l.append(finalizer) + + def removefinalizer(self, finalizer): + for l in self._arg2finish.values(): + try: + l.remove(finalizer) + except ValueError: + pass + +def rprop(attr, doc=None): + if doc is None: + doc = "%r of underlying test item" + return property(lambda x: getattr(x._request, attr), doc=doc) + +class SetupRequest: + def __init__(self, request, setupcall): + self._request = request + self._setupcall = setupcall + self._finalizers = [] + + # no getfuncargvalue(), cached_setup, applymarker helpers here + # on purpose + + function = rprop("function") + cls = rprop("cls") + instance = rprop("instance") + fspath = rprop("fspath") + keywords = rprop("keywords") + config = rprop("config", "pytest config object.") + + def addfinalizer(self, finalizer): + self._setupcall.addfinalizer(finalizer) + +class SetupCall: + """ a container/helper for managing calls to setup functions. """ + def __init__(self, funcargmanager, baseid, func, scope): + self.funcargmanager = funcargmanager + self.baseid = baseid + self.func = func + self.funcargnames = getfuncargnames(func) + self.scope = scope + self.active = False + self._finalizer = [] + + def execute(self, kwargs): + assert not self.active + self.active = True + self.func(**kwargs) + + def addfinalizer(self, finalizer): + assert self.active + self._finalizer.append(finalizer) + + def finish(self): + while self._finalizer: + func = self._finalizer.pop() + func() + # check neccesity of next commented call + self.funcargmanager.removefinalizer(self.finish) + self.active = False + +class FactoryDef: + """ A container for a factory definition. """ + def __init__(self, funcargmanager, baseid, argname, func, scope, params): + self.funcargmanager = funcargmanager + self.baseid = baseid + self.func = func + self.argname = argname + self.scope = scope + self.params = params + self.funcargnames = getfuncargnames(func) + + +def getfuncargnames(function, startindex=None): + # XXX merge with main.py's varnames + argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] + if startindex is None: + startindex = py.std.inspect.ismethod(function) and 1 or 0 + defaults = getattr(function, 'func_defaults', + getattr(function, '__defaults__', None)) or () + numdefaults = len(defaults) + if numdefaults: + return argnames[startindex:-numdefaults] + return argnames[startindex:] + +# algorithm for sorting on a per-parametrized resource setup basis + +def parametrize_sorted(items, ignore, cache, scopenum): + if scopenum >= 3: + return items + newitems = [] + olditems = [] + slicing_argparam = None + for item in items: + argparamlist = getfuncargparams(item, ignore, scopenum, cache) + if slicing_argparam is None and argparamlist: + slicing_argparam = argparamlist[0] + slicing_index = len(olditems) + if slicing_argparam in argparamlist: + newitems.append(item) + else: + olditems.append(item) + if newitems: + newignore = ignore.copy() + newignore.add(slicing_argparam) + newitems = parametrize_sorted(newitems + olditems[slicing_index:], + newignore, cache, scopenum) + old1 = parametrize_sorted(olditems[:slicing_index], newignore, + cache, scopenum+1) + return old1 + newitems + else: + olditems = parametrize_sorted(olditems, ignore, cache, scopenum+1) + return olditems + newitems + +def getfuncargparams(item, ignore, scopenum, cache): + """ return list of (arg,param) tuple, sorted by broader scope first. """ + assert scopenum < 3 # function + try: + cs = item.callspec + except AttributeError: + return [] + if scopenum == 0: + argparams = [x for x in cs.params.items() if x not in ignore + and cs._arg2scopenum[x[0]] == scopenum] + elif scopenum == 1: # module + argparams = [] + for argname, param in cs.params.items(): + if cs._arg2scopenum[argname] == scopenum: + key = (argname, param, item.fspath) + if key in ignore: + continue + argparams.append(key) + elif scopenum == 2: # class + argparams = [] + for argname, param in cs.params.items(): + if cs._arg2scopenum[argname] == scopenum: + l = cache.setdefault(item.fspath, []) + try: + i = l.index(item.cls) + except ValueError: + i = len(l) + l.append(item.cls) + key = (argname, param, item.fspath, i) + if key in ignore: + continue + argparams.append(key) + #elif scopenum == 3: + # argparams = [] + # for argname, param in cs.params.items(): + # if cs._arg2scopenum[argname] == scopenum: + # key = (argname, param, getfslineno(item.obj)) + # if key in ignore: + # continue + # argparams.append(key) + return argparams + diff -r 6b2daddff4f58e4b2d2bfe64ab850d6ec3c77d9e -r a34510e74c40c5ddd12745ff2760dd0571e879a8 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1,6 +1,6 @@ import pytest, py, sys from _pytest import python as funcargs -from _pytest.main import FuncargLookupError +from _pytest.python import FuncargLookupError class TestModule: def test_failing_import(self, testdir): https://bitbucket.org/hpk42/pytest/changeset/5c99ef9a847e/ changeset: 5c99ef9a847e user: hpk42 date: 2012-08-01 13:57:09 summary: - rename @funcarg to @factory - introduce a "testcontext" object for new-style funcargs and setup methods - New-style funcargs and setup methods cannot use the "request" object anymore. affected #: 2 files diff -r a34510e74c40c5ddd12745ff2760dd0571e879a8 -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -922,6 +922,11 @@ return self._pyfuncitem.keywords @property + def session(self): + """ pytest session object. """ + return self._pyfuncitem.session + + @property def module(self): """ module where the test function was collected. """ return self._pyfuncitem.getparent(pytest.Module).obj @@ -1029,13 +1034,18 @@ def _getfuncargvalue(self, factorydef): # collect funcargs from the factory - newnames = list(factorydef.funcargnames) - newnames.remove("request") + newnames = factorydef.funcargnames argname = factorydef.argname - factory_kwargs = {"request": self} + factory_kwargs = {} def fillfactoryargs(): for newname in newnames: - factory_kwargs[newname] = self.getfuncargvalue(newname) + if newname == "testcontext": + val = TestContextResource(self) + elif newname == "request" and not factorydef.new: + val = self + else: + val = self.getfuncargvalue(newname) + factory_kwargs[newname] = val node = self._pyfuncitem mp = monkeypatch() @@ -1237,20 +1247,22 @@ obj = getattr(holderobj, name) if not callable(obj): continue - # funcarg factories either have a pytest_funcarg__ prefix + # resource factories either have a pytest_funcarg__ prefix # or are "funcarg" marked if not callable(obj): continue - marker = getattr(obj, "funcarg", None) + marker = getattr(obj, "factory", None) if marker is not None and isinstance(marker, MarkInfo): assert not name.startswith(self._argprefix) argname = name scope = marker.kwargs.get("scope") params = marker.kwargs.get("params") + new = True elif name.startswith(self._argprefix): argname = name[len(self._argprefix):] scope = None params = None + new = False else: # no funcargs. check if we have a setup function. setup = getattr(obj, "setup", None) @@ -1260,7 +1272,8 @@ self.setuplist.append(sf) continue faclist = self.arg2facspec.setdefault(argname, []) - factorydef = FactoryDef(self, nodeid, argname, obj, scope, params) + factorydef = FactoryDef(self, nodeid, argname, obj, scope, params, + new) faclist.append(factorydef) ### check scope/params mismatch? @@ -1307,13 +1320,16 @@ for setupcall in setuplist: if setupcall.active: continue - setuprequest = SetupRequest(request, setupcall) + testcontext = TestContextSetup(request, setupcall) kwargs = {} for name in setupcall.funcargnames: - if name == "request": - kwargs[name] = setuprequest - else: + try: kwargs[name] = request.getfuncargvalue(name) + except FuncargLookupError: + if name == "testcontext": + kwargs[name] = testcontext + else: + raise scope = setupcall.scope or "function" scol = setupcall.scopeitem = request._getscopeitem(scope) self.session._setupstate.addfinalizer(setupcall.finish, scol) @@ -1332,30 +1348,67 @@ except ValueError: pass +scope2props = dict(session=()) +scope2props["module"] = ("fspath", "module") +scope2props["class"] = scope2props["module"] + ("cls",) +scope2props["function"] = scope2props["class"] + ("function", "keywords") + +def scopeprop(attr, name=None, doc=None): + if doc is None: + doc = "%s of underlying test context" % (attr,) + name = name or attr + def get(self): + if name in scope2props[self.scope]: + return getattr(self._request, name) + raise AttributeError("%s not available in %s-scoped context" % ( + name, self.scope)) + return property(get, doc=doc) + def rprop(attr, doc=None): if doc is None: - doc = "%r of underlying test item" + doc = "%s of underlying test context" % attr return property(lambda x: getattr(x._request, attr), doc=doc) -class SetupRequest: - def __init__(self, request, setupcall): +class TestContext(object): + def __init__(self, request, scope): self._request = request - self._setupcall = setupcall - self._finalizers = [] + self.scope = scope # no getfuncargvalue(), cached_setup, applymarker helpers here # on purpose - function = rprop("function") - cls = rprop("cls") - instance = rprop("instance") - fspath = rprop("fspath") - keywords = rprop("keywords") config = rprop("config", "pytest config object.") + session = rprop("session", "pytest session object.") + param = rprop("param") + + function = scopeprop("function") + module = scopeprop("module") + cls = scopeprop("class", "cls") + instance = scopeprop("instance") + fspath = scopeprop("fspath") + #keywords = scopeprop("keywords") + +class TestContextSetup(TestContext): + def __init__(self, request, setupcall): + self._setupcall = setupcall + self._finalizers = [] + super(TestContextSetup, self).__init__(request, setupcall.scope) def addfinalizer(self, finalizer): + """ Add a finalizer to be called after the last test in the + test context executes. """ self._setupcall.addfinalizer(finalizer) +class TestContextResource(TestContext): + def __init__(self, request): + super(TestContextResource, self).__init__(request, request.scope) + + def addfinalizer(self, finalizer): + """ Add a finalizer to be called after the last test in the + test context executes. """ + self._request.addfinalizer(finalizer) + + class SetupCall: """ a container/helper for managing calls to setup functions. """ def __init__(self, funcargmanager, baseid, func, scope): @@ -1386,16 +1439,17 @@ class FactoryDef: """ A container for a factory definition. """ - def __init__(self, funcargmanager, baseid, argname, func, scope, params): + def __init__(self, funcargmanager, baseid, argname, func, scope, params, + new): self.funcargmanager = funcargmanager self.baseid = baseid self.func = func self.argname = argname self.scope = scope self.params = params + self.new = new self.funcargnames = getfuncargnames(func) - def getfuncargnames(function, startindex=None): # XXX merge with main.py's varnames argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] diff -r a34510e74c40c5ddd12745ff2760dd0571e879a8 -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1595,12 +1595,12 @@ def test_receives_funcargs(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.funcarg - def arg1(request): + @pytest.mark.factory + def arg1(): return 1 - @pytest.mark.funcarg - def arg2(request, arg1): + @pytest.mark.factory + def arg2(arg1): return arg1 + 1 def test_add(arg2): @@ -1615,12 +1615,12 @@ def test_receives_funcargs_scope_mismatch(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.funcarg(scope="function") - def arg1(request): + @pytest.mark.factory(scope="function") + def arg1(): return 1 - @pytest.mark.funcarg(scope="module") - def arg2(request, arg1): + @pytest.mark.factory(scope="module") + def arg2(arg1): return arg1 + 1 def test_add(arg2): @@ -1638,13 +1638,13 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.funcarg(params=[1,2]) - def arg1(request): + @pytest.mark.factory(params=[1,2]) + def arg1(testcontext): l.append(1) - return request.param + return testcontext.param - @pytest.mark.funcarg - def arg2(request, arg1): + @pytest.mark.factory + def arg2(arg1): return arg1 + 1 def test_add(arg1, arg2): @@ -1675,7 +1675,6 @@ "*test_function*advanced*FAILED", ]) -### XXX shift to test_session.py class TestFuncargManager: def pytest_funcarg__testdir(self, request): testdir = request.getfuncargvalue("testdir") @@ -1727,14 +1726,14 @@ testdir.makeconftest(""" import pytest @pytest.mark.setup - def perfunction(request, tmpdir): + def perfunction(testcontext, tmpdir): pass - @pytest.mark.funcarg - def arg1(request, tmpdir): + @pytest.mark.factory + def arg1(tmpdir): pass @pytest.mark.setup - def perfunction2(request, arg1): + def perfunction2(arg1): pass def pytest_funcarg__fm(request): @@ -1751,10 +1750,10 @@ setupcalls, allnames = fm.getsetuplist(item.nodeid) assert len(setupcalls) == 2 assert setupcalls[0].func.__name__ == "perfunction" - assert "request" in setupcalls[0].funcargnames + assert "testcontext" in setupcalls[0].funcargnames assert "tmpdir" in setupcalls[0].funcargnames assert setupcalls[1].func.__name__ == "perfunction2" - assert "request" in setupcalls[1].funcargnames + assert "testcontext" not in setupcalls[1].funcargnames assert "arg1" in setupcalls[1].funcargnames assert "tmpdir" not in setupcalls[1].funcargnames #assert "tmpdir" in setupcalls[1].depfuncargs @@ -1768,12 +1767,12 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.funcarg(scope="module") - def arg(request): + @pytest.mark.factory(scope="module") + def arg(): l.append(1) return 0 @pytest.mark.setup(scope="class") - def something(request, arg): + def something(arg): l.append(2) def test_hello(arg): @@ -1793,12 +1792,12 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.funcarg(params=[1,2]) - def arg(request): - return request.param + @pytest.mark.factory(params=[1,2]) + def arg(testcontext): + return testcontext.param @pytest.mark.setup - def something(request, arg): + def something(arg): l.append(arg) def test_hello(): @@ -1819,13 +1818,13 @@ l = [] - @pytest.mark.funcarg(scope="session", params=[1,2]) - def arg(request): - return request.param + @pytest.mark.factory(scope="session", params=[1,2]) + def arg(testcontext): + return testcontext.param @pytest.mark.setup(scope="function") - def append(request, arg): - if request.function.__name__ == "test_some": + def append(testcontext, arg): + if testcontext.function.__name__ == "test_some": l.append(arg) def test_some(): @@ -1845,19 +1844,19 @@ l = [] - @pytest.mark.funcarg(scope="function", params=[1,2]) - def farg(request): - return request.param + @pytest.mark.factory(scope="function", params=[1,2]) + def farg(testcontext): + return testcontext.param - @pytest.mark.funcarg(scope="class", params=list("ab")) - def carg(request): - return request.param + @pytest.mark.factory(scope="class", params=list("ab")) + def carg(testcontext): + return testcontext.param @pytest.mark.setup(scope="class") - def append(request, farg, carg): + def append(testcontext, farg, carg): def fin(): l.append("fin_%s%s" % (carg, farg)) - request.addfinalizer(fin) + testcontext.addfinalizer(fin) """) testdir.makepyfile(""" import pytest @@ -1879,9 +1878,9 @@ def test_parametrize(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.funcarg(params=["a", "b", "c"]) - def arg(request): - return request.param + @pytest.mark.factory(params=["a", "b", "c"]) + def arg(testcontext): + return testcontext.param l = [] def test_param(arg): l.append(arg) @@ -1895,8 +1894,8 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.funcarg(scope="module") - def arg(request): + @pytest.mark.factory(scope="module") + def arg(): l.append(1) return 1 @@ -1917,8 +1916,8 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.funcarg(scope="module") - def arg(request): + @pytest.mark.factory(scope="module") + def arg(): l.append(1) return 1 @@ -1940,11 +1939,11 @@ import pytest finalized = [] created = [] - @pytest.mark.funcarg(scope="module") - def arg(request): + @pytest.mark.factory(scope="module") + def arg(testcontext): created.append(1) - assert request.scope == "module" - request.addfinalizer(lambda: finalized.append(1)) + assert testcontext.scope == "module" + testcontext.addfinalizer(lambda: finalized.append(1)) def pytest_funcarg__created(request): return len(created) def pytest_funcarg__finalized(request): @@ -1979,15 +1978,15 @@ import pytest finalized = [] created = [] - @pytest.mark.funcarg(scope="function") - def arg(request): + @pytest.mark.factory(scope="function") + def arg(testcontext): pass """) testdir.makepyfile( test_mod1=""" import pytest - @pytest.mark.funcarg(scope="session") - def arg(request): + @pytest.mark.factory(scope="session") + def arg(testcontext): %s def test_1(arg): pass @@ -2001,18 +2000,16 @@ def test_register_only_with_mark(self, testdir): testdir.makeconftest(""" import pytest - finalized = [] - created = [] - @pytest.mark.funcarg - def arg(request): + @pytest.mark.factory + def arg(): return 1 """) testdir.makepyfile( test_mod1=""" import pytest - @pytest.mark.funcarg - def arg(request): - return request.getfuncargvalue("arg") + 1 + @pytest.mark.factory + def arg(arg): + return arg + 1 def test_1(arg): assert arg == 2 """) @@ -2022,9 +2019,9 @@ def test_parametrize_and_scope(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.funcarg(scope="module", params=["a", "b", "c"]) - def arg(request): - return request.param + @pytest.mark.factory(scope="module", params=["a", "b", "c"]) + def arg(testcontext): + return testcontext.param l = [] def test_param(arg): l.append(arg) @@ -2040,14 +2037,14 @@ def test_scope_mismatch(self, testdir): testdir.makeconftest(""" import pytest - @pytest.mark.funcarg(scope="function") - def arg(request): + @pytest.mark.factory(scope="function") + def arg(testcontext): pass """) testdir.makepyfile(""" import pytest - @pytest.mark.funcarg(scope="session") - def arg(request, arg): + @pytest.mark.factory(scope="session") + def arg(arg): pass def test_mismatch(arg): pass @@ -2062,9 +2059,9 @@ testdir.makepyfile(""" import pytest - @pytest.mark.funcarg(scope="module", params=[1, 2]) - def arg(request): - return request.param + @pytest.mark.factory(scope="module", params=[1, 2]) + def arg(testcontext): + return testcontext.param l = [] def test_1(arg): @@ -2081,11 +2078,11 @@ testdir.makeconftest(""" import pytest - @pytest.mark.funcarg(scope="session", params="s1 s2".split()) - def sarg(request): + @pytest.mark.factory(scope="session", params="s1 s2".split()) + def sarg(): pass - @pytest.mark.funcarg(scope="module", params="m1 m2".split()) - def marg(request): + @pytest.mark.factory(scope="module", params="m1 m2".split()) + def marg(): pass """) testdir.makepyfile(test_mod1=""" @@ -2129,19 +2126,19 @@ l = [] - @pytest.mark.funcarg(scope="function", params=[1,2]) - def farg(request): - return request.param + @pytest.mark.factory(scope="function", params=[1,2]) + def farg(testcontext): + return testcontext.param - @pytest.mark.funcarg(scope="class", params=list("ab")) - def carg(request): - return request.param + @pytest.mark.factory(scope="class", params=list("ab")) + def carg(testcontext): + return testcontext.param @pytest.mark.setup(scope="class") - def append(request, farg, carg): + def append(testcontext, farg, carg): def fin(): l.append("fin_%s%s" % (carg, farg)) - request.addfinalizer(fin) + testcontext.addfinalizer(fin) """) testdir.makepyfile(""" import pytest @@ -2175,19 +2172,19 @@ testdir.makepyfile(""" import pytest - @pytest.mark.funcarg(scope="function", params=[1, 2]) - def arg(request): - param = request.param - request.addfinalizer(lambda: l.append("fin:%s" % param)) + @pytest.mark.factory(scope="function", params=[1, 2]) + def arg(testcontext): + param = testcontext.param + testcontext.addfinalizer(lambda: l.append("fin:%s" % param)) l.append("create:%s" % param) - return request.param + return testcontext.param - @pytest.mark.funcarg(scope="module", params=["mod1", "mod2"]) - def modarg(request): - param = request.param - request.addfinalizer(lambda: l.append("fin:%s" % param)) + @pytest.mark.factory(scope="module", params=["mod1", "mod2"]) + def modarg(testcontext): + param = testcontext.param + testcontext.addfinalizer(lambda: l.append("fin:%s" % param)) l.append("create:%s" % param) - return request.param + return testcontext.param l = [] def test_1(arg): @@ -2219,12 +2216,12 @@ testdir.makepyfile(""" import pytest - @pytest.mark.funcarg(scope="module", params=[1, 2]) - def arg(request): - request.config.l = l # to access from outer - x = request.param - request.addfinalizer(lambda: l.append("fin%s" % x)) - return request.param + @pytest.mark.factory(scope="module", params=[1, 2]) + def arg(testcontext): + testcontext.config.l = l # to access from outer + x = testcontext.param + testcontext.addfinalizer(lambda: l.append("fin%s" % x)) + return testcontext.param l = [] def test_1(arg): @@ -2248,11 +2245,11 @@ testdir.makepyfile(""" import pytest - @pytest.mark.funcarg(scope="function", params=[1, 2]) - def arg(request): - x = request.param - request.addfinalizer(lambda: l.append("fin%s" % x)) - return request.param + @pytest.mark.factory(scope="function", params=[1, 2]) + def arg(testcontext): + x = testcontext.param + testcontext.addfinalizer(lambda: l.append("fin%s" % x)) + return testcontext.param l = [] def test_1(arg): @@ -2270,13 +2267,13 @@ testdir.makepyfile(""" import pytest - @pytest.mark.funcarg(scope="module", params=[1, 2]) - def arg(request): - return request.param + @pytest.mark.factory(scope="module", params=[1, 2]) + def arg(testcontext): + return testcontext.param @pytest.mark.setup(scope="module") - def mysetup(request, arg): - request.addfinalizer(lambda: l.append("fin%s" % arg)) + def mysetup(testcontext, arg): + testcontext.addfinalizer(lambda: l.append("fin%s" % arg)) l.append("setup%s" % arg) l = [] @@ -2297,3 +2294,76 @@ reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=6) + at pytest.mark.parametrize(("scope", "ok", "error"),[ + ["session", "", "fspath class function module"], + ["module", "module fspath", "cls function"], + ["class", "module fspath cls", "function"], + ["function", "module fspath cls function", ""] +]) +class TestTestContextScopeAccess: + def test_setup(self, testdir, scope, ok, error): + testdir.makepyfile(""" + import pytest + @pytest.mark.setup(scope=%r) + def myscoped(testcontext): + for x in %r: + assert hasattr(testcontext, x) + for x in %r: + pytest.raises(AttributeError, lambda: + getattr(testcontext, x)) + assert testcontext.session + assert testcontext.config + def test_func(): + pass + """ %(scope, ok.split(), error.split())) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + + def test_resource(self, testdir, scope, ok, error): + testdir.makepyfile(""" + import pytest + @pytest.mark.factory(scope=%r) + def arg(testcontext): + for x in %r: + assert hasattr(testcontext, x) + for x in %r: + pytest.raises(AttributeError, lambda: + getattr(testcontext, x)) + assert testcontext.session + assert testcontext.config + def test_func(arg): + pass + """ %(scope, ok.split(), error.split())) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + +class TestTestContextVarious: + def test_newstyle_no_request(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.factory + def arg(request): + pass + def test_1(arg): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*no factory found*request*", + ]) + + def test_setupcontext_no_param(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.factory(params=[1,2]) + def arg(testcontext): + return testcontext.param + + @pytest.mark.setup + def mysetup(testcontext, arg): + assert not hasattr(testcontext, "param") + def test_1(arg): + assert arg in (1,2) + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2) https://bitbucket.org/hpk42/pytest/changeset/d130a4163d9a/ changeset: d130a4163d9a user: hpk42 date: 2012-08-01 14:52:51 summary: reshuffle docs, try to get a bit closer to release-relevant documentation affected #: 11 files diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.0.dev6' +__version__ = '2.3.0.dev7' diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1355,7 +1355,8 @@ def scopeprop(attr, name=None, doc=None): if doc is None: - doc = "%s of underlying test context" % (attr,) + doc = ("%s of underlying test context, may not exist " + "if the testcontext has a higher scope" % (attr,)) name = name or attr def get(self): if name in scope2props[self.scope]: @@ -1370,6 +1371,7 @@ return property(lambda x: getattr(x._request, attr), doc=doc) class TestContext(object): + """ Basic objects of the current testing context. """ def __init__(self, request, scope): self._request = request self.scope = scope @@ -1379,7 +1381,6 @@ config = rprop("config", "pytest config object.") session = rprop("session", "pytest session object.") - param = rprop("param") function = scopeprop("function") module = scopeprop("module") @@ -1400,6 +1401,8 @@ self._setupcall.addfinalizer(finalizer) class TestContextResource(TestContext): + param = rprop("param") + def __init__(self, request): super(TestContextResource, self).__init__(request, request.scope) diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc doc/en/builtin.txt --- a/doc/en/builtin.txt +++ b/doc/en/builtin.txt @@ -17,10 +17,9 @@ .. automodule:: pytest :members: +.. _builtinresources: -.. _builtinfuncargs: - -Builtin function arguments +Builtin resources / function arguments ----------------------------------------------------- You can ask for available builtin or project-custom diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc doc/en/conf.py --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,7 +17,7 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = release = "2.3.0.dev3" +version = release = "2.3.0.dev6" import sys, os diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc doc/en/example/newexamples.txt --- a/doc/en/example/newexamples.txt +++ /dev/null @@ -1,289 +0,0 @@ - -Scoping and parametrizing Funcarg factories ---------------------------------------------------- - -.. regendoc:wipe - -.. versionadded:: 2.3 - -The ``@pytest.mark.funcarg`` marker allows - -* to mark a function without a ``pytest_funcarg__`` as a factory -* to cause parametrization and run all tests multiple times - with the multiple created resources -* to set a scope which determines the level of caching - -Here is a simple example for defining a SMTPServer server -object with a session scope:: - - # content of conftest.py - import pytest - import smtplib - - @pytest.mark.funcarg(scope="session") - def smtp(request): - smtp = smtplib.SMTP("merlinux.eu") - request.addfinalizer(smtp.close) - return smtp - -You can now use this server connection from your tests:: - - # content of test_module.py - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - assert "merlinux" in response[1] - assert 0 # for demo purposes - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - assert 0 # for demo purposes - -If you run the tests:: - - $ py.test -q - collecting ... collected 2 items - FF - ================================= FAILURES ================================= - ________________________________ test_ehlo _________________________________ - - smtp = - - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - assert "merlinux" in response[1] - > assert 0 # for demo purposes - E assert 0 - - test_module.py:5: AssertionError - ________________________________ test_noop _________________________________ - - smtp = - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - > assert 0 # for demo purposes - E assert 0 - - test_module.py:10: AssertionError - 2 failed in 0.26 seconds - -you will see the two ``assert 0`` failing and can see that -the same (session-scoped) object was passed into the two test functions. - -If you now want to test multiple servers you can simply parametrize -the ``smtp`` factory:: - - # content of conftest.py - import pytest - import smtplib - - @pytest.mark.funcarg(scope="session", - params=["merlinux.eu", "mail.python.org"]) - def smtp(request): - smtp = smtplib.SMTP(request.param) - def fin(): - print "closing", smtp - smtp.close() - request.addfinalizer(fin) - return smtp - -Only two lines changed and no test code needs to change. Let's do -another run:: - - $ py.test -q - collecting ... collected 4 items - FFFF - ================================= FAILURES ================================= - __________________________ test_ehlo[merlinux.eu] __________________________ - - smtp = - - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - assert "merlinux" in response[1] - > assert 0 # for demo purposes - E assert 0 - - test_module.py:5: AssertionError - __________________________ test_noop[merlinux.eu] __________________________ - - smtp = - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - > assert 0 # for demo purposes - E assert 0 - - test_module.py:10: AssertionError - ________________________ test_ehlo[mail.python.org] ________________________ - - smtp = - - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - > assert "merlinux" in response[1] - E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' - - test_module.py:4: AssertionError - ________________________ test_noop[mail.python.org] ________________________ - - smtp = - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - > assert 0 # for demo purposes - E assert 0 - - test_module.py:10: AssertionError - 4 failed in 6.94 seconds - -We get four failures because we are running the two tests twice with -different ``smtp`` instantiations as defined on the factory. -Note that with the ``mail.python.org`` connection the second tests -fails already in ``test_ehlo`` because it wrongly expects a specific -server string. - -You can look at what tests pytest collects without running them:: - - $ py.test --collectonly - =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev5 - plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov - collecting ... collected 4 items - - - - - - - ============================= in 0.02 seconds ============================= - -And you can run without output capturing and minimized failure reporting to check that the ``smtp`` objects are finalized at session end:: - - $ py.test --tb=line -q -s - collecting ... collected 4 items - FFFF - ================================= FAILURES ================================= - /home/hpk/tmp/doc-exec-389/test_module.py:5: assert 0 - /home/hpk/tmp/doc-exec-389/test_module.py:10: assert 0 - /home/hpk/tmp/doc-exec-389/test_module.py:4: assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' - /home/hpk/tmp/doc-exec-389/test_module.py:10: assert 0 - 4 failed in 9.99 seconds - closing - closing - -.. _`new_setup`: - -``@pytest.mark.setup``: xUnit on steroids --------------------------------------------------------------------- - -.. regendoc:wipe - -.. versionadded:: 2.3 - -The ``@pytest.mark.setup`` marker allows - -* to define setup-functions close to test code or in conftest.py files - or plugins. -* to mark a function as a setup/fixture method; the function can itself - receive funcargs and will execute multiple times if the funcargs - are parametrized -* to set a scope which determines the level of caching and how often - the setup function is going to be called. - -Here is a simple example which configures a global funcarg without -the test needing to have it in its signature:: - - # content of conftest.py - import pytest - - @pytest.mark.funcarg(scope="module") - def resource(request, tmpdir): - def fin(): - print "finalize", tmpdir - request.addfinalizer(fin) - print "created resource", tmpdir - return tmpdir - -And the test file contains a setup function using this resource:: - - # content of test_module.py - import pytest - - @pytest.mark.setup(scope="module") - def setresource(resource): - print "setupresource", resource - global myresource - myresource = resource - - def test_1(): - assert myresource - print "using myresource", myresource - - def test_2(): - assert myresource - print "using myresource", myresource - -Let's run this module:: - - $ py.test -qs - collecting ... collected 2 items - .. - 2 passed in 0.62 seconds - created resource /home/hpk/tmp/pytest-4224/test_10 - setupresource /home/hpk/tmp/pytest-4224/test_10 - using myresource /home/hpk/tmp/pytest-4224/test_10 - using myresource /home/hpk/tmp/pytest-4224/test_10 - finalize /home/hpk/tmp/pytest-4224/test_10 - -The two test functions will see the same resource instance because it has -a module life cycle or scope. - -The resource funcarg can later add parametrization without any test -or setup code needing to change:: - - # content of conftest.py - import pytest - - @pytest.mark.funcarg(scope="module", params=["aaa", "bbb"]) - def resource(request, tmpdir): - newtmp = tmpdir.join(request.param) - def fin(): - print "finalize", newtmp - request.addfinalizer(fin) - print "created resource", newtmp - return newtmp - -Running this will run four tests:: - - $ py.test -qs - collecting ... collected 4 items - .... - 4 passed in 0.25 seconds - created resource /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa - setupresource /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa - using myresource /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa - using myresource /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa - finalize /home/hpk/tmp/pytest-4225/test_1_aaa_0/aaa - created resource /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb - setupresource /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb - using myresource /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb - using myresource /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb - finalize /home/hpk/tmp/pytest-4225/test_1_bbb_0/bbb - -Each parameter causes the creation of a respective resource and the -unchanged test module uses it in its ``@setup`` decorated method. - -.. note:: - - Parametrized Resources will be grouped together during test execution. - Moreover, any added finalizers will be run before the next parametrized - resource is being setup. diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc doc/en/example/parametrize.txt --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.txt @@ -329,87 +329,3 @@ ========================= short test summary info ========================== SKIP [27] /home/hpk/p/pytest/doc/en/example/multipython.py:36: 'python2.8' not found 48 passed, 27 skipped in 1.70 seconds - -.. regendoc:wipe - -Grouping test execution by parameter ------------------------------------------ - -By default pytest will execute test functions by executing all its parametrized invocations. If you rather want to group execution by parameter, you can -use something like the following ``conftest.py`` example. It uses -a parametrized "resource" object:: - - # content of conftest.py - def pytest_collection_modifyitems(items): - def cmp(item1, item2): - param1 = item1.callspec.getparam("resource") - param2 = item2.callspec.getparam("resource") - if param1 < param2: - return -1 - elif param1 > param2: - return 1 - return 0 - items.sort(cmp=cmp) - - def pytest_generate_tests(metafunc): - if "resource" in metafunc.funcargnames: - metafunc.parametrize("resource", [1,2], indirect=True) - - class Resource: - def __init__(self, num): - self.num = num - def finalize(self): - print "finalize", self - - def pytest_funcarg__resource(request): - return request.cached_setup(lambda: Resource(request.param), - teardown=lambda res: res.finalize(), - extrakey=request.param) - - -If you have a test file like this:: - - # content of test_resource.py - def test_hello(resource): - pass - - def test_world(resource): - pass - - class TestClass: - def test_method1(self, resource): - pass - def test_method2(self, resource): - pass - -then a subsequent execution will order the running of tests by -parameter value:: - - $ py.test -v -s - =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev3 -- /home/hpk/venv/1/bin/python - cachedir: /home/hpk/tmp/doc-exec-340/.cache - plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov - collecting ... collected 8 items - - test_resource.py:1: test_hello[1] PASSED - test_resource.py:4: test_world[1] PASSED - test_resource.py:8: TestClass.test_method1[1] PASSED - test_resource.py:10: TestClass.test_method2[1] PASSED - test_resource.py:1: test_hello[2] PASSED - test_resource.py:4: test_world[2] PASSED - test_resource.py:8: TestClass.test_method1[2] PASSED - test_resource.py:10: TestClass.test_method2[2] PASSED - - ========================= 8 passed in 0.03 seconds ========================= - finalize - finalize - -.. note:: - Despite the per-session ordering the finalize() of the session-scoped - resource executes at the end of the whole test session. The life - cycle of the two parametrized instantiated resources will thus overlap. - One possible workaround is to make the resource instantiations be - aware of each other and teardown the other one before returning a new - resource. There are plans for future releases of pytest to offer an - out-of-the-box way to support session-ordering. diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc doc/en/funcarg_compare.txt --- /dev/null +++ b/doc/en/funcarg_compare.txt @@ -0,0 +1,228 @@ + +V5: changes to new resource/setup facilities +============================================================= + +**Target audience**: Reading this document requires basic knowledge of +python testing, xUnit setup methods and the basic pytest funcarg mechanism, +see http://pytest.org/latest/funcargs.html + + +**Changes**: This V5 draft is based on incorporating and thinking about +feedback on previous versions provided by Floris Bruynooghe, Carl Meyer, +Ronny Pfannschmidt and Samuele Pedroni. I have also now implemented it +which triggered a number of refinements as well. The main changes are: + +* Collapse funcarg factory decorators into a single "@resource" one. + You can specify scopes and params with it. When using the decorator + the "pytest_funcarg__" prefix is not allowed and the old-style + ``request`` object cannot be received. + +* funcarg resource factories can now use funcargs themselves + +* Drop setup/directory scope from this draft + +* introduce a new @setup decorator similar to the @funcarg one + except that setup-markers cannot define parametriation themselves. + Instead they can easily depend on a parametrized funcarg (which + must not be visible at test function signatures). + +* drop consideration of setup_X support for funcargs because + it is less flexible and probably causes more implementation + troubles than the current @setup approach which can share + a lot of logic with the @funcarg one. + +* tests are grouped by parametrized funcargs and according to scope + (sounds like a small thing but is a big deal) + +* make the new-style funcargs/setup use a "testcontext" object + which offers test context info and addfinalizer() methods but no + getfuncargvalue()/cached_setup()/applymarker anymore. Reason + being that getfuncargvalue()/cached_setup breaks other features + such as sorting by resource-scope and parametrization + + +.. currentmodule:: _pytest + +Shortcomings of the previous pytest_funcarg__ mechanism +--------------------------------------------------------- + +The previous funcarg mechanism calls a factory each time a +funcarg for a test function is testcontexted. If a factory wants +t re-use a resource across different scopes, it often used +the ``testcontext.cached_setup()`` helper to manage caching of +resources. Here is a basic example how we could implement +a per-session Database object:: + + # content of conftest.py + class Database: + def __init__(self): + print ("database instance created") + def destroy(self): + print ("database instance destroyed") + + def pytest_funcarg__db(request): + return request.cached_setup(setup=DataBase, + teardown=lambda db: db.destroy, + scope="session") + +There are several limitations and difficulties with this approach: + +1. Scoping funcarg resource creation is not straight forward, instead one must + understand the intricate cached_setup() method mechanics. + +2. parametrizing the "db" resource is not straight forward: + you need to apply a "parametrize" decorator or implement a + :py:func:`~hookspec.pytest_generate_tests` hook + calling :py:func:`~python.Metafunc.parametrize` which + performs parametrization at the places where the resource + is used. Moreover, you need to modify the factory to use an + ``extrakey`` parameter containing ``request.param`` to the + :py:func:`~python.Request.cached_setup` call. + +3. Multiple parametrized session-scoped resources will be active + at the same time, making it hard for them to affect global state + of the application under test. + +4. there is no way how you can make use of funcarg factories + in xUnit setup methods. + +5. A non-parametrized funcarg factory cannot use a parametrized + funcarg resource if it isn't stated in the test function signature. + +All of these limitations are addressed with pytest-2.3 and its +new facilities. + +Direct scoping of funcarg factories +-------------------------------------------------------- + +Instead of calling cached_setup(), you can decorate your factory +to state its scope:: + + @pytest.mark.resource(scope="session") + def db(testcontext): + # factory will only be invoked once per session - + db = DataBase() + testcontext.addfinalizer(db.destroy) # destroy when session is finished + return db + +This factory implementation does not need to call ``cached_setup()`` anymore +because it will only be invoked once per session. Moreover, the +``testcontext.addfinalizer()`` registers a finalizer according to the specified +resource scope on which the factory function is operating. With this new +scoping, the still existing ``cached_setup()`` should be much less used +but will remain for compatibility reasons and for the case where you +still want to have your factory get called on a per-item basis. + + +Direct parametrization of funcarg resource factories +---------------------------------------------------------- + +.. note:: Implemented + +Previously, funcarg factories could not directly cause parametrization. +You needed to specify a ``@parametrize`` or implement a ``pytest_generate_tests`` hook to perform parametrization, i.e. calling a test multiple times +with different value sets. pytest-2.X introduces a decorator for use +on the factory itself:: + + @pytest.mark.resource(params=["mysql", "pg"]) + def pytest_funcarg__db(testcontext): + ... + +Here the factory will be invoked twice (with the respective "mysql" +and "pg" values set as ``testcontext.param`` attributes) and and all of +the tests requiring "db" will run twice as well. The "mysql" and +"pg" values will also be used for reporting the test-invocation variants. + +This new way of parametrizing funcarg factories should in many cases +allow to re-use already written factories because effectively +``testcontext.param`` are already the parametrization attribute for test +functions/classes were parametrized via +:py:func:`~_pytest.python.Metafunc.parametrize(indirect=True)` calls. + +Of course it's perfectly fine to combine parametrization and scoping:: + + @pytest.mark.resource(scope="session", params=["mysql", "pg"]) + def pytest_funcarg__db(testcontext): + if testcontext.param == "mysql": + db = MySQL() + elif testcontext.param == "pg": + db = PG() + testcontext.addfinalizer(db.destroy) # destroy when session is finished + return db + +This would execute all tests requiring the per-session "db" resource twice, +receiving the values created by the two respective invocations to the +factory function. + + +No ``pytest_funcarg__`` prefix when using @resource decorator +------------------------------------------------------------------- + + +.. note:: Implemented + +When using the ``@funcarg`` decorator the name of the function +does not need to (and in fact cannot) use the ``pytest_funcarg__`` +naming:: + + @pytest.mark.resource + def db(testcontext): + ... + +The name under which the funcarg resource can be requested is ``db``. + +You can also use the "old" non-decorator way of specifying funcarg factories +aka:: + + def pytest_funcarg__db(testcontext): + ... + +It is recommended to use the resource decorator, however. + + +solving per-session setup / the new @setup marker +-------------------------------------------------------------- + +.. note:: Implemented, at least working for basic situations. + +pytest for a long time offered a pytest_configure and a pytest_sessionstart +hook which are often used to setup global resources. This suffers from +several problems: + +1. in distributed testing the master process would setup test resources + that are never needed because it only co-ordinates the test run + activities of the slave processes. + +2. if you only perform a collection (with "--collectonly") + resource-setup will still be executed. + +3. If a pytest_sessionstart is contained in some subdirectories + conftest.py file, it will not be called. This stems from the + fact that this hook is actually used for reporting, in particular + the test-header with platform/custom information. + +Moreover, it is today not easy to define a scoped setup from plugins or +conftest files other than to implement a ``pytest_runtest_setup()`` hook +and caring for scoping/caching yourself. And it's virtually impossible +to do this with parametrization as ``pytest_runtest_setup()`` is called +during test execution and parametrization happens at collection time. + +It follows that pytest_configure/session/runtest_setup are often not +appropriate for implementing common fixture needs. Therefore, +pytest-2.X introduces a new "@pytest.mark.setup" marker which takes +an optional "scope" parameter. + +See :ref:`new_setup` for examples. + +funcarg and setup discovery now happens at collection time +--------------------------------------------------------------------- + +.. note:: + Partially implemented - collectonly shows no extra information however. + +pytest-2.X takes care to discover funcarg factories and @setup methods +at collection time. This is more efficient especially for large test suites. +Moreover, a call to "py.test --collectonly" should be able to show +a lot of setup-information and thus presents a nice method to get an +overview of resource management in your project. + diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc doc/en/funcargs.txt --- a/doc/en/funcargs.txt +++ b/doc/en/funcargs.txt @@ -11,6 +11,13 @@ Dependency injection through function arguments ================================================= +.. note:: + + This section describes the pytest mechanisms prior + to the pytest-2.3 release. If you haven't used these + features yet, it makes more sense to stop here and read + :ref:`resources` instead. + py.test lets you inject objects into test invocations and precisely control their life cycle in relation to the overall test execution. Moreover, you can run a test function multiple times injecting different objects. @@ -39,7 +46,7 @@ or with multiple numerical arguments sets and want to reuse the same set of test functions. -py.test comes with some :ref:`builtinfuncargs` and there are some refined usages in the examples section. +py.test comes with some :ref:`builtinresources` and there are some refined usages in the examples section. .. _funcarg: diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc doc/en/index.txt --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -25,11 +25,11 @@ - **supports functional testing and complex test setups** + - (new in 2.3) :ref:`easy test resource management and generalized xUnit setup ` - (new in 2.2) :ref:`durations` - (much improved in 2.2) :ref:`marking and test selection ` - (improved in 2.2) :ref:`parametrized test functions ` - advanced :ref:`skip and xfail` - - unique :ref:`dependency injection through funcargs ` - can :ref:`distribute tests to multiple CPUs ` through :ref:`xdist plugin ` - can :ref:`continuously re-run failing tests ` - many :ref:`builtin helpers ` diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc doc/en/resources.txt --- a/doc/en/resources.txt +++ b/doc/en/resources.txt @@ -1,271 +1,414 @@ -V4: Creating and working with parametrized resources -=============================================================== +.. _resources: -**Target audience**: Reading this document requires basic knowledge of -python testing, xUnit setup methods and the basic pytest funcarg mechanism, -see http://pytest.org/latest/funcargs.html +test resource management and xUnit setup (on steroids) +======================================================= -**Abstract**: pytest-2.X provides yet more powerful and flexible -fixture machinery by introducing: +.. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection -* a new ``@pytest.mark.funcarg`` marker to define funcarg factories and their - scoping and parametrization. No special ``pytest_funcarg__`` naming there. +.. versionadded: 2.3 -* a new ``@pytest.mark.setup`` marker to define setup functions and their +pytest offers advanced resource parametrization and injection mechanisms +including a fully integrated generalization of the popular xUnit +setup-style methods. A resource is created by a ``@pytest.mark.factory`` +marked function and its name is the name of the function. A resource +is injected into test or setup functions if they use the name +in their signature. Therefore and also for historic reasons, resources are +sometimes called "funcargs" because they ultimately appear as +function arguments. + +The pytest resource management and setup features are exposed through +three decorators: + +* a `@pytest.mark.factory`_ marker to define resource factories, + their scoping and parametrization. + +* a `@pytest.mark.setup`_ marker to define setup functions and their scoping. -* directly use funcargs through funcarg factory signatures +* a `@pytest.mark.parametrize`_ marker for executing test functions + multiple times with different parameter sets -Both funcarg factories and setup functions can be defined in test modules, -classes, conftest.py files and installed plugins. +Generally, resource factories and setup functions: -The introduction of these two markers lifts several prior limitations -and allows to easily define and implement complex testing scenarios. +- can be defined in test modules, test classes, conftest.py files or + in plugins. -Nonwithstanding these extensions, already existing test suites and plugins -written to work for previous pytest versions shall run unmodified. +- can themselves receive resources through their function arguments, + simplifying the setup and use of interdependent resources. +- can use the special `testcontext`_ object for access to the + context in which the factory/setup is called and for registering + finalizers. -**Changes**: This V4 draft is based on incorporating and thinking about -feedback on previous versions provided by Floris Bruynooghe, Carl Meyer, -Ronny Pfannschmidt and Samuele Pedroni. It remains as draft -documentation, pending further refinements and changes according to -implementation or backward compatibility issues. The main changes are: +This document showcases these features through some basic examples. -* Collapse funcarg factory decorators into a single "@funcarg" one. - You can specify scopes and params with it. When using the decorator - the "pytest_funcarg__" prefix becomes optional. +Note that pytest also comes with some :ref:`builtinresources` which +you can use without defining them yourself. -* funcarg factories can now use funcargs themselves +Background and terms +--------------------------- -* Drop setup/directory scope from this draft +The pytest resource management mechanism is an example of `Dependency +Injection`_ which helps to de-couple test code from resource +instantiation code required for them to execute. At test writing time +you typically do not need to care for the details of how your required +resources are constructed, if they live through a function, class, +module or session scope or if the test will be called multiple times +with different resource instances. -* introduce a new @setup decorator similar to the @funcarg one - except that setup-markers cannot define parametriation themselves. - Instead they can easily depend on a parametrized funcarg (which - must not be visible at test function signatures). +To create a value with which to call a test function a resource factory +function is called which gets full access to the test context and can +register finalizers which are to be run after the last test in that context +finished. Resource factories can be implemented in same test class or +test module, in a per-directory ``conftest.py`` file or in an external plugin. This allows total de-coupling of test and setup code. -* drop consideration of setup_X support for funcargs because - it is less flexible and probably causes more implementation - troubles than the current @setup approach which can share - a lot of logic with the @funcarg one. +A test function may be invoked multiple times in which case we +speak of :ref:`parametrized testing `. This can be +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 +of test functions. -* tests are grouped by parametrized funcargs -.. currentmodule:: _pytest +.. _`@pytest.mark.factory`: +``@pytest.mark.factory``: Creating parametrized, scoped resources +----------------------------------------------------------------- -Shortcomings of the previous pytest_funcarg__ mechanism ---------------------------------------------------------- +.. regendoc:wipe -The previous funcarg mechanism calls a factory each time a -funcarg for a test function is requested. If a factory wants -t re-use a resource across different scopes, it often used -the ``request.cached_setup()`` helper to manage caching of -resources. Here is a basic example how we could implement -a per-session Database object:: +.. versionadded:: 2.3 - # content of conftest.py - class Database: - def __init__(self): - print ("database instance created") - def destroy(self): - print ("database instance destroyed") +The `@pytest.mark.factory`_ marker allows to - def pytest_funcarg__db(request): - return request.cached_setup(setup=DataBase, - teardown=lambda db: db.destroy, - scope="session") +* mark a function as a factory for resources used by test and setup functions +* define parametrization to run tests multiple times with different + resource instances +* set a scope which determines the level of caching. valid scopes + are ``session``, ``module``, ``class`` and ``function``. -There are some problems with this approach: +Here is a simple example of a factory creating a shared ``smtplib.SMTP`` +connection resource which test functions then may use across the whole +test session:: -1. Scoping resource creation is not straight forward, instead one must - understand the intricate cached_setup() method mechanics. + # content of conftest.py + import pytest + import smtplib -2. parametrizing the "db" resource is not straight forward: - you need to apply a "parametrize" decorator or implement a - :py:func:`~hookspec.pytest_generate_tests` hook - calling :py:func:`~python.Metafunc.parametrize` which - performs parametrization at the places where the resource - is used. Moreover, you need to modify the factory to use an - ``extrakey`` parameter containing ``request.param`` to the - :py:func:`~python.Request.cached_setup` call. + @pytest.mark.factory(scope="session") + def smtp(testcontext): + smtp = smtplib.SMTP("merlinux.eu") + testcontext.addfinalizer(smtp.close) + return smtp -3. there is no way how you can make use of funcarg factories - in xUnit setup methods. +The name of the resource is ``smtp`` (the factory function name) +and you can now access the ``smtp`` resource by listing it as +an input parameter in any test function below the directory where +``conftest.py`` is located:: -4. A non-parametrized funcarg factory cannot use a parametrized - funcarg resource if it isn't stated in the test function signature. + # content of test_module.py + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + assert "merlinux" in response[1] + assert 0 # for demo purposes -The following sections address the advances which solve all of these problems. + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + assert 0 # for demo purposes +If you run the tests:: -Direct scoping of funcarg factories --------------------------------------------------------- + $ py.test -q + collecting ... collected 2 items + FF + ================================= FAILURES ================================= + ________________________________ test_ehlo _________________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + assert "merlinux" in response[1] + > assert 0 # for demo purposes + E assert 0 + + test_module.py:5: AssertionError + ________________________________ test_noop _________________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + 2 failed in 0.18 seconds -.. note:: Implemented +you will see the two ``assert 0`` failing and can see that +the same (session-scoped) object was passed into the two test functions. -Instead of calling cached_setup(), you can decorate your factory -to state its scope:: +If you now want to test multiple servers you can simply parametrize +the ``smtp`` factory:: - @pytest.mark.funcarg(scope="session") - def pytest_funcarg__db(request): - # factory will only be invoked once per session - - db = DataBase() - request.addfinalizer(db.destroy) # destroy when session is finished - return db + # content of conftest.py + import pytest + import smtplib -This factory implementation does not need to call ``cached_setup()`` anymore -because it will only be invoked once per session. Moreover, the -``request.addfinalizer()`` registers a finalizer according to the specified -resource scope on which the factory function is operating. With this new -scoping, the still existing ``cached_setup()`` should be much less used -but will remain for compatibility reasons and for the case where you -still want to have your factory get called on a per-item basis. + @pytest.mark.factory(scope="session", + params=["merlinux.eu", "mail.python.org"]) + def smtp(testcontext): + smtp = smtplib.SMTP(testcontext.param) + def fin(): + smtp.close() + testcontext.addfinalizer(fin) + return smtp +The main change is the definition of a ``params`` list in the +``factory``-marker and the ``testcontext.param`` access within the +factory function. No test code needs to change. So let's just do another +run:: -Direct parametrization of funcarg resource factories + $ py.test -q + collecting ... collected 4 items + FFFF + ================================= FAILURES ================================= + __________________________ test_ehlo[merlinux.eu] __________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + assert "merlinux" in response[1] + > assert 0 # for demo purposes + E assert 0 + + test_module.py:5: AssertionError + __________________________ test_noop[merlinux.eu] __________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + ________________________ test_ehlo[mail.python.org] ________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + > assert "merlinux" in response[1] + E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + + test_module.py:4: AssertionError + ________________________ test_noop[mail.python.org] ________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + 4 failed in 6.42 seconds + +We get four failures because we are running the two tests twice with +different ``smtp`` instantiations as defined on the factory. +Note that with the ``mail.python.org`` connection the second test +fails in ``test_ehlo`` because it expects a specific server string. + +You can also look at what tests pytest collects without running them:: + + $ py.test --collectonly + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev7 + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + collecting ... collected 4 items + + + + + + + ============================= in 0.02 seconds ============================= + +Note that pytest orders your test run by resource usage, minimizing +the number of active resources at any given time. + + +Accessing resources from a factory function ---------------------------------------------------------- -.. note:: Implemented +You can directly use resources as funcargs in resource factories. +Extending the previous example we can instantiate an application +object and stick the live ``smtp`` resource into it:: -Previously, funcarg factories could not directly cause parametrization. -You needed to specify a ``@parametrize`` or implement a ``pytest_generate_tests`` hook to perform parametrization, i.e. calling a test multiple times -with different value sets. pytest-2.X introduces a decorator for use -on the factory itself:: + # content of test_appsetup.py + + import pytest - @pytest.mark.funcarg(params=["mysql", "pg"]) - def pytest_funcarg__db(request): - ... + class App: + def __init__(self, smtp): + self.smtp = smtp -Here the factory will be invoked twice (with the respective "mysql" -and "pg" values set as ``request.param`` attributes) and and all of -the tests requiring "db" will run twice as well. The "mysql" and -"pg" values will also be used for reporting the test-invocation variants. + @pytest.mark.factory(scope="module") + def app(smtp): + return App(smtp) -This new way of parametrizing funcarg factories should in many cases -allow to re-use already written factories because effectively -``request.param`` are already the parametrization attribute for test -functions/classes were parametrized via -:py:func:`~_pytest.python.Metafunc.parametrize(indirect=True)` calls. + def test_exists(app): + assert app.smtp -Of course it's perfectly fine to combine parametrization and scoping:: +Let's run this:: - @pytest.mark.funcarg(scope="session", params=["mysql", "pg"]) - def pytest_funcarg__db(request): - if request.param == "mysql": - db = MySQL() - elif request.param == "pg": - db = PG() - request.addfinalizer(db.destroy) # destroy when session is finished - return db + $ py.test -v test_appsetup.py + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev7 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-398/.cache + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + collecting ... collected 2 items + + test_appsetup.py:12: test_exists[merlinux.eu] PASSED + test_appsetup.py:12: test_exists[mail.python.org] PASSED + + ========================= 2 passed in 5.96 seconds ========================= -This would execute all tests requiring the per-session "db" resource twice, -receiving the values created by the two respective invocations to the -factory function. +Due to the parametrization of ``smtp`` the test will +run twice with two different ``App`` instances and respective smtp servers. +There is no need for the ``app`` factory to be aware of the parametrization. -Direct usage of funcargs with funcargs factories ----------------------------------------------------------- -.. note:: Implemented. +.. _`new_setup`: +.. _`@pytest.mark.setup`: -You can now directly use funcargs in funcarg factories. Example:: +``@pytest.mark.setup``: xUnit setup methods on steroids +----------------------------------------------------------------- - @pytest.mark.funcarg(scope="session") - def db(request, tmpdir): - # tmpdir is a session-specific tempdir +.. regendoc:wipe -Apart from convenience it also solves an issue when your factory -depends on a parametrized funcarg. Previously, a call to -``request.getfuncargvalue()`` happens at test execution time and -thus pytest would not know at collection time about the fact that -a required resource is parametrized. +.. versionadded:: 2.3 -No ``pytest_funcarg__`` prefix when using @funcarg decorator -------------------------------------------------------------------- +The ``@pytest.mark.setup`` marker allows +* to define setup-functions close to test code or in conftest.py files + or plugins. +* to mark a function as a setup method; the function can itself + receive funcargs and will execute multiple times if the funcargs + are parametrized +* to set a scope which influences when the setup function going to be + called. valid scopes are ``session``, ``module``, ``class`` and ``function``. -.. note:: Implemented +Here is a simple example. First we define a global ``globdir`` resource:: -When using the ``@funcarg`` decorator the name of the function -does not need to (and in fact cannot) use the ``pytest_funcarg__`` -naming:: + # content of conftest.py + import pytest - @pytest.mark.funcarg - def db(request): - ... + @pytest.mark.factory(scope="module") + def globdir(testcontext, tmpdir): + def fin(): + print "finalize", tmpdir + testcontext.addfinalizer(fin) + print "created resource", tmpdir + return tmpdir -The name under which the funcarg resource can be requested is ``db``. +And then we write a test file containing a setup-marked function +taking this resource and setting it as a module global:: -You can also use the "old" non-decorator way of specifying funcarg factories -aka:: + # content of test_module.py + import pytest - def pytest_funcarg__db(request): - ... + @pytest.mark.setup(scope="module") + def setresource(testcontext, globdir): + print "setupresource", globdir + testcontext.module.myresource = globdir -It is recommended to use the funcarg-decorator, however. + def test_1(): + assert myresource + print "using myresource", myresource + def test_2(): + assert myresource + print "using myresource", myresource -solving per-session setup / the new @setup marker --------------------------------------------------------------- +Let's run this module:: -.. note:: Implemented, at least working for basic situations. + $ py.test -qs + collecting ... collected 2 items + .. + 2 passed in 0.26 seconds + created resource /home/hpk/tmp/pytest-4427/test_10 + setupresource /home/hpk/tmp/pytest-4427/test_10 + using myresource /home/hpk/tmp/pytest-4427/test_10 + using myresource /home/hpk/tmp/pytest-4427/test_10 + finalize /home/hpk/tmp/pytest-4427/test_10 -pytest for a long time offered a pytest_configure and a pytest_sessionstart -hook which are often used to setup global resources. This suffers from -several problems: +The two test functions in the module use the same global ``myresource`` +object because the ``setresource`` set it as a module attribute. -1. in distributed testing the master process would setup test resources - that are never needed because it only co-ordinates the test run - activities of the slave processes. +The ``globdir`` factory can now become parametrized without any test +or setup code needing to change:: -2. if you only perform a collection (with "--collectonly") - resource-setup will still be executed. + # content of conftest.py + import pytest -3. If a pytest_sessionstart is contained in some subdirectories - conftest.py file, it will not be called. This stems from the - fact that this hook is actually used for reporting, in particular - the test-header with platform/custom information. + @pytest.mark.factory(scope="module", params=["aaa", "bbb"]) + def globdir(testcontext, tmpdir): + newtmp = tmpdir.join(testcontext.param) + def fin(): + print "finalize", newtmp + testcontext.addfinalizer(fin) + print "created resource", newtmp + return newtmp -Moreover, it is today not easy to define a scoped setup from plugins or -conftest files other than to implement a ``pytest_runtest_setup()`` hook -and caring for scoping/caching yourself. And it's virtually impossible -to do this with parametrization as ``pytest_runtest_setup()`` is called -during test execution and parametrization happens at collection time. +Running the unchanged previous test files now runs four tests:: -It follows that pytest_configure/session/runtest_setup are often not -appropriate for implementing common fixture needs. Therefore, -pytest-2.X introduces a new "@pytest.mark.setup" marker which takes -an optional "scope" parameter. + $ py.test -qs + collecting ... collected 4 items + .... + 4 passed in 0.26 seconds + created resource /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa + setupresource /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa + using myresource /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa + using myresource /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa + finalize /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa + created resource /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb + setupresource /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb + using myresource /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb + using myresource /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb + finalize /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb -See :ref:`new_setup` for examples. +Each parameter causes the creation of a respective resource and the +unchanged test module uses it in its ``@setup`` decorated method. -funcarg and setup discovery now happens at collection time ---------------------------------------------------------------------- +.. note:: -.. note:: - Partially implemented - collectonly shows no extra information however. - -pytest-2.X takes care to discover funcarg factories and @setup methods -at collection time. This is more efficient especially for large test suites. -Moreover, a call to "py.test --collectonly" should be able to show -a lot of setup-information and thus presents a nice method to get an -overview of resource management in your project. - + Tests using a particular parametrized resource instance will + executed next to each other. Any finalizers will be run before the + next parametrized resource instance is being setup and tests + are rerun. Grouping tests by resource parameters ---------------------------------------------------------- -.. note:: Implemented. +.. regendoc: wipe -pytest used to always sort test items by their source location. -With pytest-2.X tests are first grouped by funcarg parameters. -If you have a parametrized funcarg, then all the tests using it -will first execute with it. Then any finalizers are called and then -the next parametrized resource instance is created and its tests are run. -Among other things, this eases testing of applications which create -and use global state. +pytest minimizes the number of active resources during test runs. +If you have a parametrized resource, then all the tests using one +resource instance will execute one after another. Then any finalizers +are called for that resource instance and then the next parametrized +resource instance is created and its tests are run. Among other things, +this eases testing of applications which create and use global state. The following example uses two parametrized funcargs, one of which is scoped on a per-module basis:: @@ -273,18 +416,18 @@ # content of test_module.py import pytest - @pytest.mark.funcarg(scope="module", params=["mod1", "mod2"]) - def modarg(request): - param = request.param + @pytest.mark.factory(scope="module", params=["mod1", "mod2"]) + def modarg(testcontext): + param = testcontext.param print "create", param def fin(): print "fin", param - request.addfinalizer(fin) + testcontext.addfinalizer(fin) return param - @pytest.mark.funcarg(scope="function", params=[1,2]) - def otherarg(request): - return request.param + @pytest.mark.factory(scope="function", params=[1,2]) + def otherarg(testcontext): + return testcontext.param def test_0(otherarg): print " test0", otherarg @@ -297,8 +440,8 @@ $ py.test -v -s =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev5 -- /home/hpk/venv/1/bin/python - cachedir: /home/hpk/tmp/doc-exec-388/.cache + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev7 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-398/.cache plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 8 items @@ -325,10 +468,132 @@ test2 2 mod2 fin mod2 -You can see that that the parametrized ``modarg`` resource lead to -a re-ordering of test execution. The finalizer for the "mod1" parametrized -resource was executed before the "mod2" resource was setup. +You can see that the parametrized module-scoped ``modarg`` resource caused +a re-ordering of test execution. The finalizer for the ``mod1`` parametrized +resource was executed before the ``mod2`` resource was setup. -.. note:: +.. currentmodule:: _pytest.python +.. _`testcontext`: - The current implementation is experimental. +``testcontext``: interacting with test context +--------------------------------------------------- + +The ``testcontext`` object may be received by `@pytest.mark.factory`_ or +`@pytest.mark.setup`_ marked functions. It contains information relating +to the test context within which the function executes. Moreover, you +can call ``testcontext.addfinalizer(myfinalizer)`` in order to trigger +a call to ``myfinalizer`` after the last test in the test context has executed. +If passed to a parametrized factory ``testcontext.param`` will contain +a parameter (one value out of the ``params`` list specified with the +`@pytest.mark.factory`_ marker). + +.. autoclass:: _pytest.python.TestContext() + :members: + +.. _`@pytest.mark.parametrize`: + +``@pytest.mark.parametrize``: directly parametrizing test functions +---------------------------------------------------------------------------- + +.. versionadded:: 2.2 + +The builtin ``pytest.mark.parametrize`` decorator enables +parametrization of arguments for a test function. Here is an example +of a test function that wants check for expected output given a certain input:: + + # content of test_expectation.py + import pytest + @pytest.mark.parametrize(("input", "expected"), [ + ("3+5", 8), + ("2+4", 6), + ("6*9", 42), + ]) + def test_eval(input, expected): + assert eval(input) == expected + +we parametrize two arguments of the test function so that the test +function is called three times. Let's run it:: + + $ py.test -q + collecting ... collected 11 items + ..F........ + ================================= FAILURES ================================= + ____________________________ test_eval[6*9-42] _____________________________ + + input = '6*9', expected = 42 + + @pytest.mark.parametrize(("input", "expected"), [ + ("3+5", 8), + ("2+4", 6), + ("6*9", 42), + ]) + def test_eval(input, expected): + > assert eval(input) == expected + E assert 54 == 42 + E + where 54 = eval('6*9') + + test_expectation.py:8: AssertionError + 1 failed, 10 passed in 0.04 seconds + +As expected only one pair of input/output values fails the simple test function. + +Note that there are various ways how you can mark groups of functions, +see :ref:`mark`. + +Generating parameters combinations, depending on command line +---------------------------------------------------------------------------- + +.. regendoc:wipe + +Let's say we want to execute a test with different computation +parameters and the parameter range shall be determined by a command +line argument. Let's first write a simple (do-nothing) computation test:: + + # content of test_compute.py + + def test_compute(param1): + assert param1 < 4 + +Now we add a test configuration like this:: + + # content of conftest.py + + def pytest_addoption(parser): + parser.addoption("--all", action="store_true", + help="run all combinations") + + def pytest_generate_tests(metafunc): + if 'param1' in metafunc.funcargnames: + if metafunc.config.option.all: + end = 5 + else: + end = 2 + metafunc.parametrize("param1", range(end)) + +This means that we only run 2 tests if we do not pass ``--all``:: + + $ py.test -q test_compute.py + collecting ... collected 2 items + .. + 2 passed in 0.03 seconds + +We run only two computations, so we see two dots. +let's run the full monty:: + + $ py.test -q --all + collecting ... collected 5 items + ....F + ================================= FAILURES ================================= + _____________________________ test_compute[4] ______________________________ + + param1 = 4 + + def test_compute(param1): + > assert param1 < 4 + E assert 4 < 4 + + test_compute.py:3: AssertionError + 1 failed, 4 passed in 0.03 seconds + +As expected when running the full range of ``param1`` values +we'll get an error on the last one. diff -r 5c99ef9a847e12001a5a3bf88c73d743a027ac7b -r d130a4163d9a833f55d4fc913d0e676e869b44bc setup.py --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.0.dev6', + version='2.3.0.dev7', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Aug 2 12:08:03 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Thu, 02 Aug 2012 10:08:03 -0000 Subject: [py-svn] commit/pytest: hpk42: refine documentation, move setup to own "setup" page and provide Message-ID: <20120802100803.22791.96266@bitbucket05.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/e9293dbade35/ changeset: e9293dbade35 user: hpk42 date: 2012-08-02 12:07:54 summary: refine documentation, move setup to own "setup" page and provide some more examples. move old setup_module/... to xunit_old page. affected #: 5 files diff -r d130a4163d9a833f55d4fc913d0e676e869b44bc -r e9293dbade357ca7ac20a796f0f3484ae70b06ab doc/en/index.txt --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -25,7 +25,9 @@ - **supports functional testing and complex test setups** - - (new in 2.3) :ref:`easy test resource management and generalized xUnit setup ` + - (new in 2.3) :ref:`easy test resource management, scoping and + parametrization ` + - (new in 2.3) :ref:`xunitsetup`. - (new in 2.2) :ref:`durations` - (much improved in 2.2) :ref:`marking and test selection ` - (improved in 2.2) :ref:`parametrized test functions ` diff -r d130a4163d9a833f55d4fc913d0e676e869b44bc -r e9293dbade357ca7ac20a796f0f3484ae70b06ab doc/en/resources.txt --- a/doc/en/resources.txt +++ b/doc/en/resources.txt @@ -1,74 +1,66 @@ .. _resources: -test resource management and xUnit setup (on steroids) +======================================================= +test resource injection and parametrization ======================================================= .. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection .. versionadded: 2.3 -pytest offers advanced resource parametrization and injection mechanisms -including a fully integrated generalization of the popular xUnit -setup-style methods. A resource is created by a ``@pytest.mark.factory`` -marked function and its name is the name of the function. A resource -is injected into test or setup functions if they use the name -in their signature. Therefore and also for historic reasons, resources are -sometimes called "funcargs" because they ultimately appear as -function arguments. - -The pytest resource management and setup features are exposed through -three decorators: - -* a `@pytest.mark.factory`_ marker to define resource factories, - their scoping and parametrization. - -* a `@pytest.mark.setup`_ marker to define setup functions and their - scoping. - -* a `@pytest.mark.parametrize`_ marker for executing test functions - multiple times with different parameter sets - -Generally, resource factories and setup functions: - -- can be defined in test modules, test classes, conftest.py files or - in plugins. - -- can themselves receive resources through their function arguments, - simplifying the setup and use of interdependent resources. - -- can use the special `testcontext`_ object for access to the - context in which the factory/setup is called and for registering - finalizers. - -This document showcases these features through some basic examples. - -Note that pytest also comes with some :ref:`builtinresources` which -you can use without defining them yourself. - -Background and terms ---------------------------- +pytest offers very flexible means for managing test resources and +test parametrization. The pytest resource management mechanism is an example of `Dependency -Injection`_ which helps to de-couple test code from resource -instantiation code required for them to execute. At test writing time -you typically do not need to care for the details of how your required -resources are constructed, if they live through a function, class, -module or session scope or if the test will be called multiple times -with different resource instances. +Injection`_ because test and :ref:`setup functions ` receive +resources simply by stating them as an input argument. Therefore and +also for historic reasons, they are often called **funcargs**. At test +writing time you typically do not need to care for the details of how +your required resources are constructed, if they live through a +function, class, module or session scope or if the test will be called +multiple times with different resource instances. To create a value with which to call a test function a resource factory function is called which gets full access to the test context and can register finalizers which are to be run after the last test in that context finished. Resource factories can be implemented in same test class or -test module, in a per-directory ``conftest.py`` file or in an external plugin. This allows total de-coupling of test and setup code. +test module, in a per-directory ``conftest.py`` file or in an external +plugin. This allows **total de-coupling of test and setup code**, +lowering the cost of refactoring. A test function may be invoked multiple times in which case we -speak of :ref:`parametrized testing `. This can be -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 -of test functions. +speak of parametrization. You can parametrize resources or parametrize +test function arguments directly or even implement your own parametrization +scheme through a plugin hook. +A resource has a **name** under which test and setup functions +can access it by listing it as an input argument. Due to this and +also for historic reasons, resources are often called **funcargs**. +A resource is created by a factory which can be flagged with a **scope** +to only create resources on a per-class/per-module/per-session basis +instead of the default per-function scope. + +Concretely, there are three means of resource and parametrization management: + +* a `@pytest.mark.factory`_ marker to define resource factories, + their scoping and parametrization. Factories can themselves + receive resources through their function arguments, easing + the setup of interdependent resources. They can also use + the special `testcontext`_ object to access details n which + the factory/setup is called and for registering finalizers. + +* a `@pytest.mark.parametrize`_ marker for executing test functions + multiple times with different parameter sets + +* a `pytest_generate_tests`_ plugin hook marker for implementing + your parametrization for a test function which may depend on + command line options, class/module attributes etc. + +Finally, pytest comes with some :ref:`builtinresources` which +you can use without defining them yourself. Moreover, third-party +plugins offer their own resources so that after installation +you can simply use them in your test and setup functions. .. _`@pytest.mark.factory`: @@ -81,11 +73,12 @@ The `@pytest.mark.factory`_ marker allows to -* mark a function as a factory for resources used by test and setup functions -* define parametrization to run tests multiple times with different +* mark a function as a factory for resources, useable by test and setup functions +* define parameters in order to run tests multiple times with different resource instances -* set a scope which determines the level of caching. valid scopes - are ``session``, ``module``, ``class`` and ``function``. +* set a scope which determines the level of caching, i.e. how often + the factory will be called. Valid scopes are ``session``, ``module``, + ``class`` and ``function``. Here is a simple example of a factory creating a shared ``smtplib.SMTP`` connection resource which test functions then may use across the whole @@ -248,12 +241,12 @@ the number of active resources at any given time. -Accessing resources from a factory function +Interdepdendent resources ---------------------------------------------------------- -You can directly use resources as funcargs in resource factories. -Extending the previous example we can instantiate an application -object and stick the live ``smtp`` resource into it:: +You can not only use resources in test functions but also in resource factories +themselves. Extending the previous example we can instantiate an application +object by sticking the ``smtp`` resource into it:: # content of test_appsetup.py @@ -289,114 +282,6 @@ There is no need for the ``app`` factory to be aware of the parametrization. -.. _`new_setup`: -.. _`@pytest.mark.setup`: - -``@pytest.mark.setup``: xUnit setup methods on steroids ------------------------------------------------------------------ - -.. regendoc:wipe - -.. versionadded:: 2.3 - -The ``@pytest.mark.setup`` marker allows - -* to define setup-functions close to test code or in conftest.py files - or plugins. -* to mark a function as a setup method; the function can itself - receive funcargs and will execute multiple times if the funcargs - are parametrized -* to set a scope which influences when the setup function going to be - called. valid scopes are ``session``, ``module``, ``class`` and ``function``. - -Here is a simple example. First we define a global ``globdir`` resource:: - - # content of conftest.py - import pytest - - @pytest.mark.factory(scope="module") - def globdir(testcontext, tmpdir): - def fin(): - print "finalize", tmpdir - testcontext.addfinalizer(fin) - print "created resource", tmpdir - return tmpdir - -And then we write a test file containing a setup-marked function -taking this resource and setting it as a module global:: - - # content of test_module.py - import pytest - - @pytest.mark.setup(scope="module") - def setresource(testcontext, globdir): - print "setupresource", globdir - testcontext.module.myresource = globdir - - def test_1(): - assert myresource - print "using myresource", myresource - - def test_2(): - assert myresource - print "using myresource", myresource - -Let's run this module:: - - $ py.test -qs - collecting ... collected 2 items - .. - 2 passed in 0.26 seconds - created resource /home/hpk/tmp/pytest-4427/test_10 - setupresource /home/hpk/tmp/pytest-4427/test_10 - using myresource /home/hpk/tmp/pytest-4427/test_10 - using myresource /home/hpk/tmp/pytest-4427/test_10 - finalize /home/hpk/tmp/pytest-4427/test_10 - -The two test functions in the module use the same global ``myresource`` -object because the ``setresource`` set it as a module attribute. - -The ``globdir`` factory can now become parametrized without any test -or setup code needing to change:: - - # content of conftest.py - import pytest - - @pytest.mark.factory(scope="module", params=["aaa", "bbb"]) - def globdir(testcontext, tmpdir): - newtmp = tmpdir.join(testcontext.param) - def fin(): - print "finalize", newtmp - testcontext.addfinalizer(fin) - print "created resource", newtmp - return newtmp - -Running the unchanged previous test files now runs four tests:: - - $ py.test -qs - collecting ... collected 4 items - .... - 4 passed in 0.26 seconds - created resource /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa - setupresource /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa - using myresource /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa - using myresource /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa - finalize /home/hpk/tmp/pytest-4428/test_1_aaa_0/aaa - created resource /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb - setupresource /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb - using myresource /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb - using myresource /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb - finalize /home/hpk/tmp/pytest-4428/test_1_bbb_0/bbb - -Each parameter causes the creation of a respective resource and the -unchanged test module uses it in its ``@setup`` decorated method. - -.. note:: - - Tests using a particular parametrized resource instance will - executed next to each other. Any finalizers will be run before the - next parametrized resource instance is being setup and tests - are rerun. Grouping tests by resource parameters ---------------------------------------------------------- @@ -540,7 +425,10 @@ Note that there are various ways how you can mark groups of functions, see :ref:`mark`. -Generating parameters combinations, depending on command line + +.. _`pytest_generate_tests`: + +``pytest_generate_test``: implementing your own parametrization scheme ---------------------------------------------------------------------------- .. regendoc:wipe diff -r d130a4163d9a833f55d4fc913d0e676e869b44bc -r e9293dbade357ca7ac20a796f0f3484ae70b06ab doc/en/setup.txt --- /dev/null +++ b/doc/en/setup.txt @@ -0,0 +1,204 @@ +.. _xunitsetup: +.. _setup: + +``@setup`` functions or: xunit on steroids +======================================================== + +.. versionadded:: 2.3 + +.. _`funcargs`: funcargs.html +.. _`test parametrization`: funcargs.html#parametrizing-tests +.. _`unittest plugin`: plugin/unittest.html +.. _`xUnit`: http://en.wikipedia.org/wiki/XUnit + +Python, Java and many other languages support a so called xUnit_ style +of resource setup. This typically involves the call of a ``setup`` +("fixture") method before running a test function and ``teardown`` after +it has finished. Unlike :ref:`injected resources ` setup +functions work indirectly by causing global side effects or +setting test case attributes which test methods can then access. + +pytest originally introduced in 2005 a fine-grained model of detecting +setup and teardown functions on a per-module, class or function basis. +The Python unittest module and nose have subsequently incorporated them. +This model remains supported as :ref:`old-style xunit`. + +With pytest-2.3 a new ``pytest.setup()`` decorator is introduced +to mark functions as setup functions which: + +- can receive resources through funcargs, +- fully interoperate with parametrized resources, +- can be defined in a plugin or conftest.py file and get called + on a per-session, per-module, per-class or per-function basis, +- can access the full :ref:`testcontext` for which the setup is called, +- can precisely control teardown by registering one or multiple + teardown functions as soon as they have performed some actions + which need undoing, eliminating the no need for a separate + "teardown" decorator. +- allow to separate different setup concerns even if they + happen to work in the same scope + +All of these features are now demonstrated by little examples. + +.. _`new_setup`: +.. _`@pytest.mark.setup`: + +basic per-function setup +------------------------------- + +.. regendoc:wipe + +Suppose you want to have a clean directory with a single +file entry for each test function in a module and have +the test execute with this directory as current working dir:: + + # content of test_funcdir.py + import pytest + import os + + @pytest.mark.setup() + def mydir(tmpdir): + tmpdir.join("myfile").write("example content") + old = tmpdir.chdir() + + def test_function1(): + assert os.path.exists("myfile") + f = open("anotherfile", "w") + f.write("") + f.close() + + def test_function2(): + assert os.path.exists("myfile") + assert not os.path.exists("anotherfile") + +Our ``mydir`` setup function is executed on a per-function basis, +the default scope used by the ``pytest.mark.setup`` decorator. +It accesses the ``tmpdir`` resource which provides a new empty +directory path object. The ``test_function2`` here checks that +it executes with a fresh directory and specifically +does not see the previously created ``anotherfile``. We can +thus expect two passing tests:: + + $ py.test -v + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev7 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-410/.cache + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + collecting ... collected 2 items + + test_funcdir.py:9: test_function1 PASSED + test_funcdir.py:15: test_function2 PASSED + + ========================= 2 passed in 0.26 seconds ========================= + +per-function setup, for every function of a project +------------------------------------------------------------ + +If you want to define a setup per-function but want to apply +it to every function in your project you don't need to duplicate +the setup-definition into each test module. Instead you can put +it into a ``conftest.py`` file into the root of your project:: + + # content of conftest.py + import pytest + import os + + @pytest.mark.setup() + def cleandir(tmpdir): + old = tmpdir.chdir() + +The ``cleandir`` setup function will be called for every test function +below the directory tree where ``conftest.py`` resides. In this +case it just uses the builtin ``tmpdir`` resource to change to the +empty directory ahead of running a test. + +test modules accessing a global resource +------------------------------------------------------- + +If you want test modules to access a global resource, +you can stick the resource to the module globals in +a per-module setup function. We use a :ref:`resource factory +<@pytest.mark.factory>` to create our global resource:: + + # content of conftest.py + import pytest + + class GlobalResource: + def __init__(self): + pass + + @pytest.mark.factory(scope="session") + def globresource(): + return GlobalResource() + + @pytest.mark.setup(scope="module") + def setresource(testcontext, globresource): + testcontext.module.globresource = globresource + +Now any test module can access ``globresource`` as a module global:: + + # content of test_glob.py + + def test_1(): + print ("test_1 %s" % globresource) + def test_2(): + print ("test_2 %s" % globresource) + +Let's run this module without output-capturing:: + + $ py.test -qs test_glob.py + collecting ... collected 2 items + .. + 2 passed in 0.02 seconds + test_1 + test_2 + +The two tests see the same global ``globresource`` object. + +Parametrizing the global resource ++++++++++++++++++++++++++++++++++++++++++++++++++ + +We extend the previous example and add parametrization to the globresource +factory and also add a finalizer:: + + # content of conftest.py + + import pytest + + class GlobalResource: + def __init__(self, param): + self.param = param + + @pytest.mark.factory(scope="session", params=[1,2]) + def globresource(testcontext): + g = GlobalResource(testcontext.param) + def fin(): + print "finalizing", g + testcontext.addfinalizer(fin) + return g + + @pytest.mark.setup(scope="module") + def setresource(testcontext, globresource): + testcontext.module.globresource = globresource + +And then re-run our test module:: + + $ py.test -qs test_glob.py + collecting ... collected 4 items + .... + 4 passed in 0.02 seconds + test_1 + test_2 + finalizing + test_1 + test_2 + finalizing + +We are now running the two tests twice with two different global resource +instances. Note that the tests are ordered such that only +one instance is active at any given time: the finalizer of +the first globresource instance is called before the second +instance is created and sent to the setup functions. + + + diff -r d130a4163d9a833f55d4fc913d0e676e869b44bc -r e9293dbade357ca7ac20a796f0f3484ae70b06ab doc/en/xunit_old.txt --- /dev/null +++ b/doc/en/xunit_old.txt @@ -0,0 +1,76 @@ + +.. _`old-style xunit`: + +Old-style xunit-style setup +======================================== + +This section describes the old way how you can implement setup and +teardown on a per-module/class/function basis. It remains fully +supported but it is recommended to rather use :ref:`@setup functions ` +or :ref:`injected resources ` for implementing your setup needs. + +Module level setup/teardown +-------------------------------------- + +If you have multiple test functions and test classes in a single +module you can optionally implement the following fixture methods +which will usually be called once for all the functions:: + + def setup_module(module): + """ setup any state specific to the execution of the given module.""" + + def teardown_module(module): + """ teardown any state that was previously setup with a setup_module + method. + """ + +Class level setup/teardown +---------------------------------- + +Similarly, the following methods are called at class level before +and after all test methods of the class are called:: + + @classmethod + def setup_class(cls): + """ setup any state specific to the execution of the given class (which + usually contains tests). + """ + + @classmethod + def teardown_class(cls): + """ teardown any state that was previously setup with a call to + setup_class. + """ + +Method and function level setup/teardown +----------------------------------------------- + +Similarly, the following methods are called around each method invocation:: + + def setup_method(self, method): + """ setup any state tied to the execution of the given method in a + class. setup_method is invoked for every test method of a class. + """ + + def teardown_method(self, method): + """ teardown any state that was previously setup with a setup_method + call. + """ + +If you would rather define test functions directly at module level +you can also use the following functions to implement fixtures:: + + def setup_function(function): + """ setup any state tied to the execution of the given function. + Invoked for every test function in the module. + """ + + def teardown_function(function): + """ teardown any state that was previously setup with a setup_function + call. + """ + +Note that it is possible for setup/teardown pairs to be invoked multiple times +per testing process. + +.. _`unittest.py module`: http://docs.python.org/library/unittest.html diff -r d130a4163d9a833f55d4fc913d0e676e869b44bc -r e9293dbade357ca7ac20a796f0f3484ae70b06ab doc/en/xunit_setup.txt --- a/doc/en/xunit_setup.txt +++ /dev/null @@ -1,83 +0,0 @@ -.. _xunitsetup: - -==================================== -Extended xUnit style setup fixtures -==================================== - -.. _`funcargs`: funcargs.html -.. _`test parametrization`: funcargs.html#parametrizing-tests -.. _`unittest plugin`: plugin/unittest.html -.. _`xUnit`: http://en.wikipedia.org/wiki/XUnit - -Python, Java and many other languages support xUnit_ style testing. -This typically involves the call of a ``setup`` ("fixture") method -before running a test function and ``teardown`` after it has finished. -``py.test`` supports a more fine-grained model of setup/teardown -handling by optionally calling per-module and per-class hooks. - - -Module level setup/teardown -============================================= - -If you have multiple test functions and test classes in a single -module you can optionally implement the following fixture methods -which will usually be called once for all the functions:: - - def setup_module(module): - """ setup any state specific to the execution of the given module.""" - - def teardown_module(module): - """ teardown any state that was previously setup with a setup_module - method. - """ - -Class level setup/teardown -============================================= - -Similarly, the following methods are called at class level before -and after all test methods of the class are called:: - - @classmethod - def setup_class(cls): - """ setup any state specific to the execution of the given class (which - usually contains tests). - """ - - @classmethod - def teardown_class(cls): - """ teardown any state that was previously setup with a call to - setup_class. - """ - -Method and function level setup/teardown -============================================= - -Similarly, the following methods are called around each method invocation:: - - def setup_method(self, method): - """ setup any state tied to the execution of the given method in a - class. setup_method is invoked for every test method of a class. - """ - - def teardown_method(self, method): - """ teardown any state that was previously setup with a setup_method - call. - """ - -If you would rather define test functions directly at module level -you can also use the following functions to implement fixtures:: - - def setup_function(function): - """ setup any state tied to the execution of the given function. - Invoked for every test function in the module. - """ - - def teardown_function(function): - """ teardown any state that was previously setup with a setup_function - call. - """ - -Note that it is possible for setup/teardown pairs to be invoked multiple times -per testing process. - -.. _`unittest.py module`: http://docs.python.org/library/unittest.html Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Aug 2 12:42:15 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Thu, 02 Aug 2012 10:42:15 -0000 Subject: [py-svn] commit/pytest: hpk42: move pytest.mark.factory/setup to pytest.factory/setup, as per flub 's suggestion Message-ID: <20120802104215.6286.98142@bitbucket15.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/e811b2a68198/ changeset: e811b2a68198 user: hpk42 date: 2012-08-02 12:41:46 summary: move pytest.mark.factory/setup to pytest.factory/setup, as per flub 's suggestion affected #: 8 files diff -r e9293dbade357ca7ac20a796f0f3484ae70b06ab -r e811b2a6819856b7db636f3bff7a7510164f2d97 _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.0.dev7' +__version__ = '2.3.0.dev8' diff -r e9293dbade357ca7ac20a796f0f3484ae70b06ab -r e811b2a6819856b7db636f3bff7a7510164f2d97 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -6,11 +6,45 @@ from _pytest.main import getfslineno from _pytest.monkeypatch import monkeypatch from py._code.code import TerminalRepr -from _pytest.mark import MarkInfo import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() +class FactoryMarker: + def __init__(self, scope, params): + self.scope = scope + self.params = params + def __call__(self, function): + function._pytestfactory = self + return function + +class SetupMarker: + def __init__(self, scope): + self.scope = scope + def __call__(self, function): + function._pytestsetup = self + return function + +# XXX a test fails when scope="function" how it should be, investigate +def factory(scope=None, params=None): + """ return a decorator to mark functions as resource factories. + + :arg scope: the scope for which this resource is shared, one of + "function", "class", "module", "session". Defaults to "function". + :arg params: an optional list of parameters which will cause multiple + invocations of tests depending on the resource. + """ + return FactoryMarker(scope, params) + +def setup(scope="function"): + """ return a decorator to mark functions as setup functions. + + :arg scope: the scope for which the setup function will be active, one + of "function", "class", "module", "session". + Defaults to "function". + """ + return SetupMarker(scope) + def cached_property(f): """returns a cached property that is calculated by function f. taken from http://code.activestate.com/recipes/576563-cached-property/""" @@ -78,6 +112,8 @@ def pytest_namespace(): raises.Exception = pytest.fail.Exception return { + 'factory': factory, + 'setup': setup, 'raises' : raises, 'collect': { 'Module': Module, 'Class': Class, 'Instance': Instance, @@ -1251,12 +1287,12 @@ # or are "funcarg" marked if not callable(obj): continue - marker = getattr(obj, "factory", None) - if marker is not None and isinstance(marker, MarkInfo): + marker = getattr(obj, "_pytestfactory", None) + if marker is not None: assert not name.startswith(self._argprefix) argname = name - scope = marker.kwargs.get("scope") - params = marker.kwargs.get("params") + scope = marker.scope + params = marker.params new = True elif name.startswith(self._argprefix): argname = name[len(self._argprefix):] @@ -1265,9 +1301,9 @@ new = False else: # no funcargs. check if we have a setup function. - setup = getattr(obj, "setup", None) - if setup is not None and isinstance(setup, MarkInfo): - scope = setup.kwargs.get("scope") + setup = getattr(obj, "_pytestsetup", None) + if setup is not None: + scope = setup.scope sf = SetupCall(self, nodeid, obj, scope) self.setuplist.append(sf) continue diff -r e9293dbade357ca7ac20a796f0f3484ae70b06ab -r e811b2a6819856b7db636f3bff7a7510164f2d97 doc/en/conf.py --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,7 +17,7 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = release = "2.3.0.dev6" +version = release = "2.3.0.dev8" import sys, os diff -r e9293dbade357ca7ac20a796f0f3484ae70b06ab -r e811b2a6819856b7db636f3bff7a7510164f2d97 doc/en/funcarg_compare.txt --- a/doc/en/funcarg_compare.txt +++ b/doc/en/funcarg_compare.txt @@ -209,7 +209,7 @@ It follows that pytest_configure/session/runtest_setup are often not appropriate for implementing common fixture needs. Therefore, -pytest-2.X introduces a new "@pytest.mark.setup" marker which takes +pytest-2.X introduces a new :ref:`@pytest.setup` marker which takes an optional "scope" parameter. See :ref:`new_setup` for examples. diff -r e9293dbade357ca7ac20a796f0f3484ae70b06ab -r e811b2a6819856b7db636f3bff7a7510164f2d97 doc/en/resources.txt --- a/doc/en/resources.txt +++ b/doc/en/resources.txt @@ -43,7 +43,7 @@ Concretely, there are three means of resource and parametrization management: -* a `@pytest.mark.factory`_ marker to define resource factories, +* a `@pytest.factory`_ marker to define resource factories, their scoping and parametrization. Factories can themselves receive resources through their function arguments, easing the setup of interdependent resources. They can also use @@ -62,16 +62,16 @@ plugins offer their own resources so that after installation you can simply use them in your test and setup functions. -.. _`@pytest.mark.factory`: +.. _`@pytest.factory`: -``@pytest.mark.factory``: Creating parametrized, scoped resources +``@pytest.factory``: Creating parametrized, scoped resources ----------------------------------------------------------------- .. regendoc:wipe .. versionadded:: 2.3 -The `@pytest.mark.factory`_ marker allows to +The `@pytest.factory`_ marker allows to * mark a function as a factory for resources, useable by test and setup functions * define parameters in order to run tests multiple times with different @@ -88,7 +88,7 @@ import pytest import smtplib - @pytest.mark.factory(scope="session") + @pytest.factory(scope="session") def smtp(testcontext): smtp = smtplib.SMTP("merlinux.eu") testcontext.addfinalizer(smtp.close) @@ -152,7 +152,7 @@ import pytest import smtplib - @pytest.mark.factory(scope="session", + @pytest.factory(scope="session", params=["merlinux.eu", "mail.python.org"]) def smtp(testcontext): smtp = smtplib.SMTP(testcontext.param) @@ -256,7 +256,7 @@ def __init__(self, smtp): self.smtp = smtp - @pytest.mark.factory(scope="module") + @pytest.factory(scope="module") def app(smtp): return App(smtp) @@ -301,7 +301,7 @@ # content of test_module.py import pytest - @pytest.mark.factory(scope="module", params=["mod1", "mod2"]) + @pytest.factory(scope="module", params=["mod1", "mod2"]) def modarg(testcontext): param = testcontext.param print "create", param @@ -310,7 +310,7 @@ testcontext.addfinalizer(fin) return param - @pytest.mark.factory(scope="function", params=[1,2]) + @pytest.factory(scope="function", params=[1,2]) def otherarg(testcontext): return testcontext.param @@ -363,14 +363,14 @@ ``testcontext``: interacting with test context --------------------------------------------------- -The ``testcontext`` object may be received by `@pytest.mark.factory`_ or -`@pytest.mark.setup`_ marked functions. It contains information relating +The ``testcontext`` object may be received by `@pytest.factory`_ or +`@pytest.setup`_ marked functions. It contains information relating to the test context within which the function executes. Moreover, you can call ``testcontext.addfinalizer(myfinalizer)`` in order to trigger a call to ``myfinalizer`` after the last test in the test context has executed. If passed to a parametrized factory ``testcontext.param`` will contain a parameter (one value out of the ``params`` list specified with the -`@pytest.mark.factory`_ marker). +`@pytest.factory`_ marker). .. autoclass:: _pytest.python.TestContext() :members: diff -r e9293dbade357ca7ac20a796f0f3484ae70b06ab -r e811b2a6819856b7db636f3bff7a7510164f2d97 doc/en/setup.txt --- a/doc/en/setup.txt +++ b/doc/en/setup.txt @@ -41,7 +41,7 @@ All of these features are now demonstrated by little examples. .. _`new_setup`: -.. _`@pytest.mark.setup`: +.. _`@pytest.setup`: basic per-function setup ------------------------------- @@ -56,7 +56,7 @@ import pytest import os - @pytest.mark.setup() + @pytest.setup() def mydir(tmpdir): tmpdir.join("myfile").write("example content") old = tmpdir.chdir() @@ -72,7 +72,7 @@ assert not os.path.exists("anotherfile") Our ``mydir`` setup function is executed on a per-function basis, -the default scope used by the ``pytest.mark.setup`` decorator. +the default scope used by the ``pytest.setup`` decorator. It accesses the ``tmpdir`` resource which provides a new empty directory path object. The ``test_function2`` here checks that it executes with a fresh directory and specifically @@ -103,7 +103,7 @@ import pytest import os - @pytest.mark.setup() + @pytest.setup() def cleandir(tmpdir): old = tmpdir.chdir() @@ -118,7 +118,7 @@ If you want test modules to access a global resource, you can stick the resource to the module globals in a per-module setup function. We use a :ref:`resource factory -<@pytest.mark.factory>` to create our global resource:: +<@pytest.factory>` to create our global resource:: # content of conftest.py import pytest @@ -127,11 +127,11 @@ def __init__(self): pass - @pytest.mark.factory(scope="session") + @pytest.factory(scope="session") def globresource(): return GlobalResource() - @pytest.mark.setup(scope="module") + @pytest.setup(scope="module") def setresource(testcontext, globresource): testcontext.module.globresource = globresource @@ -169,7 +169,7 @@ def __init__(self, param): self.param = param - @pytest.mark.factory(scope="session", params=[1,2]) + @pytest.factory(scope="session", params=[1,2]) def globresource(testcontext): g = GlobalResource(testcontext.param) def fin(): @@ -177,7 +177,7 @@ testcontext.addfinalizer(fin) return g - @pytest.mark.setup(scope="module") + @pytest.setup(scope="module") def setresource(testcontext, globresource): testcontext.module.globresource = globresource diff -r e9293dbade357ca7ac20a796f0f3484ae70b06ab -r e811b2a6819856b7db636f3bff7a7510164f2d97 setup.py --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.0.dev7', + version='2.3.0.dev8', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r e9293dbade357ca7ac20a796f0f3484ae70b06ab -r e811b2a6819856b7db636f3bff7a7510164f2d97 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1581,7 +1581,7 @@ result = testdir.makeconftest(""" import pytest - @pytest.mark.setup + @pytest.setup() def mysetup(testcontext): testcontext.uses_funcarg("db") """) @@ -1595,11 +1595,11 @@ def test_receives_funcargs(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.factory + @pytest.factory() def arg1(): return 1 - @pytest.mark.factory + @pytest.factory() def arg2(arg1): return arg1 + 1 @@ -1615,11 +1615,11 @@ def test_receives_funcargs_scope_mismatch(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.factory(scope="function") + @pytest.factory(scope="function") def arg1(): return 1 - @pytest.mark.factory(scope="module") + @pytest.factory(scope="module") def arg2(arg1): return arg1 + 1 @@ -1638,12 +1638,12 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.factory(params=[1,2]) + @pytest.factory(params=[1,2]) def arg1(testcontext): l.append(1) return testcontext.param - @pytest.mark.factory + @pytest.factory() def arg2(arg1): return arg1 + 1 @@ -1725,14 +1725,14 @@ testdir = request.getfuncargvalue("testdir") testdir.makeconftest(""" import pytest - @pytest.mark.setup + @pytest.setup() def perfunction(testcontext, tmpdir): pass - @pytest.mark.factory + @pytest.factory() def arg1(tmpdir): pass - @pytest.mark.setup + @pytest.setup() def perfunction2(arg1): pass @@ -1767,11 +1767,11 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.factory(scope="module") + @pytest.factory(scope="module") def arg(): l.append(1) return 0 - @pytest.mark.setup(scope="class") + @pytest.setup(scope="class") def something(arg): l.append(2) @@ -1792,11 +1792,11 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.factory(params=[1,2]) + @pytest.factory(params=[1,2]) def arg(testcontext): return testcontext.param - @pytest.mark.setup + @pytest.setup() def something(arg): l.append(arg) @@ -1818,11 +1818,11 @@ l = [] - @pytest.mark.factory(scope="session", params=[1,2]) + @pytest.factory(scope="session", params=[1,2]) def arg(testcontext): return testcontext.param - @pytest.mark.setup(scope="function") + @pytest.setup(scope="function") def append(testcontext, arg): if testcontext.function.__name__ == "test_some": l.append(arg) @@ -1844,15 +1844,15 @@ l = [] - @pytest.mark.factory(scope="function", params=[1,2]) + @pytest.factory(scope="function", params=[1,2]) def farg(testcontext): return testcontext.param - @pytest.mark.factory(scope="class", params=list("ab")) + @pytest.factory(scope="class", params=list("ab")) def carg(testcontext): return testcontext.param - @pytest.mark.setup(scope="class") + @pytest.setup(scope="class") def append(testcontext, farg, carg): def fin(): l.append("fin_%s%s" % (carg, farg)) @@ -1878,7 +1878,7 @@ def test_parametrize(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.factory(params=["a", "b", "c"]) + @pytest.factory(params=["a", "b", "c"]) def arg(testcontext): return testcontext.param l = [] @@ -1894,7 +1894,7 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.factory(scope="module") + @pytest.factory(scope="module") def arg(): l.append(1) return 1 @@ -1916,7 +1916,7 @@ testdir.makepyfile(""" import pytest l = [] - @pytest.mark.factory(scope="module") + @pytest.factory(scope="module") def arg(): l.append(1) return 1 @@ -1939,7 +1939,7 @@ import pytest finalized = [] created = [] - @pytest.mark.factory(scope="module") + @pytest.factory(scope="module") def arg(testcontext): created.append(1) assert testcontext.scope == "module" @@ -1978,14 +1978,14 @@ import pytest finalized = [] created = [] - @pytest.mark.factory(scope="function") + @pytest.factory(scope="function") def arg(testcontext): pass """) testdir.makepyfile( test_mod1=""" import pytest - @pytest.mark.factory(scope="session") + @pytest.factory(scope="session") def arg(testcontext): %s def test_1(arg): @@ -2000,14 +2000,14 @@ def test_register_only_with_mark(self, testdir): testdir.makeconftest(""" import pytest - @pytest.mark.factory + @pytest.factory() def arg(): return 1 """) testdir.makepyfile( test_mod1=""" import pytest - @pytest.mark.factory + @pytest.factory() def arg(arg): return arg + 1 def test_1(arg): @@ -2019,7 +2019,7 @@ def test_parametrize_and_scope(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.factory(scope="module", params=["a", "b", "c"]) + @pytest.factory(scope="module", params=["a", "b", "c"]) def arg(testcontext): return testcontext.param l = [] @@ -2037,13 +2037,13 @@ def test_scope_mismatch(self, testdir): testdir.makeconftest(""" import pytest - @pytest.mark.factory(scope="function") + @pytest.factory(scope="function") def arg(testcontext): pass """) testdir.makepyfile(""" import pytest - @pytest.mark.factory(scope="session") + @pytest.factory(scope="session") def arg(arg): pass def test_mismatch(arg): @@ -2059,7 +2059,7 @@ testdir.makepyfile(""" import pytest - @pytest.mark.factory(scope="module", params=[1, 2]) + @pytest.factory(scope="module", params=[1, 2]) def arg(testcontext): return testcontext.param @@ -2078,10 +2078,10 @@ testdir.makeconftest(""" import pytest - @pytest.mark.factory(scope="session", params="s1 s2".split()) + @pytest.factory(scope="session", params="s1 s2".split()) def sarg(): pass - @pytest.mark.factory(scope="module", params="m1 m2".split()) + @pytest.factory(scope="module", params="m1 m2".split()) def marg(): pass """) @@ -2126,15 +2126,15 @@ l = [] - @pytest.mark.factory(scope="function", params=[1,2]) + @pytest.factory(scope="function", params=[1,2]) def farg(testcontext): return testcontext.param - @pytest.mark.factory(scope="class", params=list("ab")) + @pytest.factory(scope="class", params=list("ab")) def carg(testcontext): return testcontext.param - @pytest.mark.setup(scope="class") + @pytest.setup(scope="class") def append(testcontext, farg, carg): def fin(): l.append("fin_%s%s" % (carg, farg)) @@ -2172,14 +2172,14 @@ testdir.makepyfile(""" import pytest - @pytest.mark.factory(scope="function", params=[1, 2]) + @pytest.factory(scope="function", params=[1, 2]) def arg(testcontext): param = testcontext.param testcontext.addfinalizer(lambda: l.append("fin:%s" % param)) l.append("create:%s" % param) return testcontext.param - @pytest.mark.factory(scope="module", params=["mod1", "mod2"]) + @pytest.factory(scope="module", params=["mod1", "mod2"]) def modarg(testcontext): param = testcontext.param testcontext.addfinalizer(lambda: l.append("fin:%s" % param)) @@ -2216,7 +2216,7 @@ testdir.makepyfile(""" import pytest - @pytest.mark.factory(scope="module", params=[1, 2]) + @pytest.factory(scope="module", params=[1, 2]) def arg(testcontext): testcontext.config.l = l # to access from outer x = testcontext.param @@ -2245,7 +2245,7 @@ testdir.makepyfile(""" import pytest - @pytest.mark.factory(scope="function", params=[1, 2]) + @pytest.factory(scope="function", params=[1, 2]) def arg(testcontext): x = testcontext.param testcontext.addfinalizer(lambda: l.append("fin%s" % x)) @@ -2267,11 +2267,11 @@ testdir.makepyfile(""" import pytest - @pytest.mark.factory(scope="module", params=[1, 2]) + @pytest.factory(scope="module", params=[1, 2]) def arg(testcontext): return testcontext.param - @pytest.mark.setup(scope="module") + @pytest.setup(scope="module") def mysetup(testcontext, arg): testcontext.addfinalizer(lambda: l.append("fin%s" % arg)) l.append("setup%s" % arg) @@ -2304,7 +2304,7 @@ def test_setup(self, testdir, scope, ok, error): testdir.makepyfile(""" import pytest - @pytest.mark.setup(scope=%r) + @pytest.setup(scope=%r) def myscoped(testcontext): for x in %r: assert hasattr(testcontext, x) @@ -2322,7 +2322,7 @@ def test_resource(self, testdir, scope, ok, error): testdir.makepyfile(""" import pytest - @pytest.mark.factory(scope=%r) + @pytest.factory(scope=%r) def arg(testcontext): for x in %r: assert hasattr(testcontext, x) @@ -2341,7 +2341,7 @@ def test_newstyle_no_request(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.factory + @pytest.factory() def arg(request): pass def test_1(arg): @@ -2355,11 +2355,11 @@ def test_setupcontext_no_param(self, testdir): testdir.makepyfile(""" import pytest - @pytest.mark.factory(params=[1,2]) + @pytest.factory(params=[1,2]) def arg(testcontext): return testcontext.param - @pytest.mark.setup + @pytest.setup() def mysetup(testcontext, arg): assert not hasattr(testcontext, "param") def test_1(arg): Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Aug 3 19:08:39 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Fri, 03 Aug 2012 17:08:39 -0000 Subject: [py-svn] commit/pytest: hpk42: majorly refine funcargs docs and rename "resources.txt" to "funcargs.txt" so that existing web links will eventually land at this new page when pytest is released. Also integrated the detailed reasoning and update setup function docs Message-ID: <20120803170839.3242.43844@bitbucket15.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/49dfa381fa59/ changeset: 49dfa381fa59 user: hpk42 date: 2012-08-03 19:08:27 summary: majorly refine funcargs docs and rename "resources.txt" to "funcargs.txt" so that existing web links will eventually land at this new page when pytest is released. Also integrated the detailed reasoning and update setup function docs to reflect latest discussions and feedback gathered on py-dev mailing list. affected #: 11 files diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -907,12 +907,15 @@ class FuncargRequest: - """ A request for function arguments from a test function. + """ (old-style) 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. + hook, the attribute will not be present. Note that + as of pytest-2.3 you probably rather want to use the + testcontext object and mark your factory with a ``@pytest.factory`` + marker. """ def __init__(self, pyfuncitem): diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/apiref.txt --- a/doc/en/apiref.txt +++ b/doc/en/apiref.txt @@ -11,6 +11,8 @@ customize.txt assert.txt funcargs.txt + funcarg_compare.txt + setup.txt xunit_setup.txt capture.txt monkeypatch.txt diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/conf.py --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,7 +17,7 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = release = "2.3.0.dev8" +version = release = "2.3.0.dev9" import sys, os diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/example/index.txt --- a/doc/en/example/index.txt +++ b/doc/en/example/index.txt @@ -21,4 +21,3 @@ markers.txt pythoncollection.txt nonpython.txt - newexamples.txt diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/funcarg_compare.txt --- a/doc/en/funcarg_compare.txt +++ b/doc/en/funcarg_compare.txt @@ -1,55 +1,23 @@ -V5: changes to new resource/setup facilities +.. _`funcargcompare`: + +============================================================= +pytest-2.3: reasoning for the new funcarg and setup functions ============================================================= **Target audience**: Reading this document requires basic knowledge of -python testing, xUnit setup methods and the basic pytest funcarg mechanism, -see http://pytest.org/latest/funcargs.html - - -**Changes**: This V5 draft is based on incorporating and thinking about -feedback on previous versions provided by Floris Bruynooghe, Carl Meyer, -Ronny Pfannschmidt and Samuele Pedroni. I have also now implemented it -which triggered a number of refinements as well. The main changes are: - -* Collapse funcarg factory decorators into a single "@resource" one. - You can specify scopes and params with it. When using the decorator - the "pytest_funcarg__" prefix is not allowed and the old-style - ``request`` object cannot be received. - -* funcarg resource factories can now use funcargs themselves - -* Drop setup/directory scope from this draft - -* introduce a new @setup decorator similar to the @funcarg one - except that setup-markers cannot define parametriation themselves. - Instead they can easily depend on a parametrized funcarg (which - must not be visible at test function signatures). - -* drop consideration of setup_X support for funcargs because - it is less flexible and probably causes more implementation - troubles than the current @setup approach which can share - a lot of logic with the @funcarg one. - -* tests are grouped by parametrized funcargs and according to scope - (sounds like a small thing but is a big deal) - -* make the new-style funcargs/setup use a "testcontext" object - which offers test context info and addfinalizer() methods but no - getfuncargvalue()/cached_setup()/applymarker anymore. Reason - being that getfuncargvalue()/cached_setup breaks other features - such as sorting by resource-scope and parametrization - +python testing, xUnit setup methods and the (previous) basic pytest +funcarg mechanism, see http://pytest.org/2.2.4/funcargs.html .. currentmodule:: _pytest Shortcomings of the previous pytest_funcarg__ mechanism ---------------------------------------------------------- +=========================================================== -The previous funcarg mechanism calls a factory each time a -funcarg for a test function is testcontexted. If a factory wants -t re-use a resource across different scopes, it often used -the ``testcontext.cached_setup()`` helper to manage caching of +The pre pytest-2.3 funcarg mechanism calls a factory each time a +funcarg for a test function is required. If a factory wants to +re-use a resource across different scopes, it often used +the ``request.cached_setup()`` helper to manage caching of resources. Here is a basic example how we could implement a per-session Database object:: @@ -95,10 +63,9 @@ Direct scoping of funcarg factories -------------------------------------------------------- -Instead of calling cached_setup(), you can decorate your factory -to state its scope:: +Instead of calling cached_setup(), you can use the :ref:`@pytest.factory <@pytest.factory>` decorator and directly state the scope:: - @pytest.mark.resource(scope="session") + @pytest.factory(scope="session") def db(testcontext): # factory will only be invoked once per session - db = DataBase() @@ -108,25 +75,21 @@ This factory implementation does not need to call ``cached_setup()`` anymore because it will only be invoked once per session. Moreover, the ``testcontext.addfinalizer()`` registers a finalizer according to the specified -resource scope on which the factory function is operating. With this new -scoping, the still existing ``cached_setup()`` should be much less used -but will remain for compatibility reasons and for the case where you -still want to have your factory get called on a per-item basis. +resource scope on which the factory function is operating. Direct parametrization of funcarg resource factories ---------------------------------------------------------- -.. note:: Implemented +Previously, funcarg factories could not directly cause parametrization. +You needed to specify a ``@parametrize`` decorator on your test function +or implement a ``pytest_generate_tests`` hook to perform +parametrization, i.e. calling a test multiple times with different value +sets. pytest-2.3 introduces a decorator for use on the factory itself:: -Previously, funcarg factories could not directly cause parametrization. -You needed to specify a ``@parametrize`` or implement a ``pytest_generate_tests`` hook to perform parametrization, i.e. calling a test multiple times -with different value sets. pytest-2.X introduces a decorator for use -on the factory itself:: - - @pytest.mark.resource(params=["mysql", "pg"]) + @pytest.factory(params=["mysql", "pg"]) def pytest_funcarg__db(testcontext): - ... + ... # use testcontext.param Here the factory will be invoked twice (with the respective "mysql" and "pg" values set as ``testcontext.param`` attributes) and and all of @@ -141,7 +104,7 @@ Of course it's perfectly fine to combine parametrization and scoping:: - @pytest.mark.resource(scope="session", params=["mysql", "pg"]) + @pytest.factory(scope="session", params=["mysql", "pg"]) def pytest_funcarg__db(testcontext): if testcontext.param == "mysql": db = MySQL() @@ -155,26 +118,23 @@ factory function. -No ``pytest_funcarg__`` prefix when using @resource decorator +No ``pytest_funcarg__`` prefix when using @factory decorator ------------------------------------------------------------------- +When using the ``@funcarg`` decorator the name of the function +denotes the name under which the resource can be accessed as a function +argument:: -.. note:: Implemented - -When using the ``@funcarg`` decorator the name of the function -does not need to (and in fact cannot) use the ``pytest_funcarg__`` -naming:: - - @pytest.mark.resource + @pytest.factory() def db(testcontext): ... The name under which the funcarg resource can be requested is ``db``. -You can also use the "old" non-decorator way of specifying funcarg factories +You can still use the "old" non-decorator way of specifying funcarg factories aka:: - def pytest_funcarg__db(testcontext): + def pytest_funcarg__db(request): ... It is recommended to use the resource decorator, however. @@ -183,8 +143,6 @@ solving per-session setup / the new @setup marker -------------------------------------------------------------- -.. note:: Implemented, at least working for basic situations. - pytest for a long time offered a pytest_configure and a pytest_sessionstart hook which are often used to setup global resources. This suffers from several problems: @@ -201,7 +159,7 @@ fact that this hook is actually used for reporting, in particular the test-header with platform/custom information. -Moreover, it is today not easy to define a scoped setup from plugins or +Moreover, it was not easy to define a scoped setup from plugins or conftest files other than to implement a ``pytest_runtest_setup()`` hook and caring for scoping/caching yourself. And it's virtually impossible to do this with parametrization as ``pytest_runtest_setup()`` is called @@ -209,20 +167,17 @@ It follows that pytest_configure/session/runtest_setup are often not appropriate for implementing common fixture needs. Therefore, -pytest-2.X introduces a new :ref:`@pytest.setup` marker which takes -an optional "scope" parameter. +pytest-2.3 introduces a new :ref:`@pytest.setup ` marker +for setup functions and it accepts an optional "scope" parameter. -See :ref:`new_setup` for examples. +See :ref:`setup` for more explanation and examples. funcarg and setup discovery now happens at collection time --------------------------------------------------------------------- -.. note:: - Partially implemented - collectonly shows no extra information however. - -pytest-2.X takes care to discover funcarg factories and @setup methods +pytest-2.3 takes care to discover funcarg factories and @setup methods at collection time. This is more efficient especially for large test suites. -Moreover, a call to "py.test --collectonly" should be able to show -a lot of setup-information and thus presents a nice method to get an +Moreover, a call to "py.test --collectonly" should be able to in the future +show a lot of setup-information and thus presents a nice method to get an overview of resource management in your project. diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/funcargs.txt --- a/doc/en/funcargs.txt +++ b/doc/en/funcargs.txt @@ -1,75 +1,110 @@ -============================================================== -Injecting objects into test functions (funcargs) -============================================================== -.. currentmodule:: _pytest.python - - +.. _resources: .. _`funcargs`: .. _`funcarg mechanism`: -Dependency injection through function arguments -================================================= +======================================================= +funcargs: resource injection and parametrization +======================================================= .. note:: - This section describes the pytest mechanisms prior - to the pytest-2.3 release. If you haven't used these - features yet, it makes more sense to stop here and read - :ref:`resources` instead. - -py.test lets you inject objects into test invocations and precisely -control their life cycle in relation to the overall test execution. Moreover, -you can run a test function multiple times injecting different objects. - -The basic mechanism for injecting objects is also called the -*funcarg mechanism* because objects are ultimately injected -by calling a test function with it as an argument. Unlike the -classical xUnit approach *funcargs* relate more to `Dependency Injection`_ -because they help to de-couple test code from objects required for -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. + pytest-2.3 introduces major refinements to the original funcarg + mechanism introduced to pytest-2.0. While the old way + remains fully supported, it is recommended to use the refined + mechanisms. See also the `compatibility notes`_ and the detailed + :ref:`reasoning for the new funcarg and setup functions `. .. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection -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 -register finalizers or invoke lifecycle-caching helpers. The factory -can be implemented in same test class or test module, in a -per-directory ``conftest.py`` file or in an external plugin. This -allows total de-coupling of test and setup code. +Introduction +==================== -A test function may be invoked multiple times in which case we -speak of :ref:`parametrized testing `. This can be -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 -of test functions. +py.test supports the injection of objects into test and setup functions +and flexibly control their life cycle in relation to the overall test +execution. Moreover, you can run a test function multiple times +injecting different objects. -py.test comes with some :ref:`builtinresources` and there are some refined usages in the examples section. +The basic mechanism for injecting objects is called the *funcarg +mechanism* because objects are **injected** when a test or setup +function states it as an **argument**. The injected argument is +created by a call to a registered **funcarg factory**. This approach is +an example of `Dependency Injection`_ and helps to de-couple test code +from the setup of required objects: at test writing time you do not need +to care for the details of where and how your required resources are +constructed, if they are shared on a per-class, module or session basis, +or if your test function is invoked multiple times with differently +configured resource instances. -.. _funcarg: +When a test function is invoked multiple times with different arguments we +speak of **parametrized testing**. This is useful if you want to test +e.g. against different database backends or want to write a parametrized +test function, checking that certain inputs lead to certain outputs. +You can parametrize funcarg factories, parametrize test function +arguments or even implement your own parametrization scheme through a +plugin hook. -Basic injection example --------------------------------- +pytest additionally offers powerful xunit-style :ref:`setup functions ` for the cases where you need to create implicit test state +that is not passed explicitely to test functions. -Let's look at a simple self-contained test module:: +Concretely, there are three main means of funcarg management: + +* a `@pytest.factory`_ marker to define resource factories, + their scoping and parametrization. Factories can themselves + receive resources through their function arguments, easing + the setup of `interdependent resources`_. Factories can use + the special `testcontext`_ object to access details from where + the factory or setup function is called and for registering finalizers. + +* a `@pytest.mark.parametrize`_ marker for executing test functions + multiple times with different argument sets, + +* a `pytest_generate_tests`_ plugin hook marker for implementing + your parametrization for a test function which may depend on + command line options, class/module attributes etc. + +Apart from making it easy to manage your own test resources +pytest also comes with some :ref:`builtinresources` which +you can use without defining them yourself. Third-party plugins +offer yet more domain-specific funcarg resources (for example the +`pytest-django plugin `_) so +that after plugin installation you can simply use them in +your test and setup functions. This all contributes to high +re-useability of test resource management and goes far beyond what can +be done with the classical xUnit style approach which encodes resource +setup statically into the test source code, leading to duplicate and +hard-to change fixtures. + +.. _`@pytest.factory`: + +``@pytest.factory``: Creating parametrized, scoped resources +===================================================================== + +Basic funcarg injection example +----------------------------------------------------------- + +Let's look at a simple self-contained test module using a factory +and a funcarg:: # content of ./test_simplefactory.py - def pytest_funcarg__myfuncarg(request): + import pytest + + @pytest.factory() + def myfuncarg(): return 42 def test_function(myfuncarg): assert myfuncarg == 17 -This test function needs an injected object named ``myfuncarg``. -py.test will automatically discover and call the ``pytest_funcarg__myfuncarg`` -factory. Running the test looks like this:: +Here, the ``test_function`` needs an object named ``myfuncarg`` and thus +py.test will discover and call the ``@pytest.factory`` marked ``myfuncarg`` +factory function. Running the tests looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 - plugins: xdist, bugzilla, cache, oejskit, pep8, cov + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 1 items test_simplefactory.py F @@ -83,7 +118,7 @@ > assert myfuncarg == 17 E assert 42 == 17 - test_simplefactory.py:5: AssertionError + test_simplefactory.py:8: AssertionError ========================= 1 failed in 0.02 seconds ========================= This shows that the test function was called with a ``myfuncarg`` @@ -93,14 +128,12 @@ 1. py.test :ref:`finds ` the ``test_function`` because of the ``test_`` prefix. The test function needs a function argument named ``myfuncarg``. A matching factory function is discovered by - looking for the name ``pytest_funcarg__myfuncarg``. + looking for a factory function named ``myfuncarg``. -2. ``pytest_funcarg__myfuncarg(request)`` is called and - returns the value for ``myfuncarg``. +2. ``myfuncarg()`` is called to create a value ``42``. -3. the test function can now be called: ``test_function(42)``. - This results in the above exception because of the assertion - mismatch. +3. ``test_function(42)`` is now called and results in the above + reported exception because of the assertion mismatch. Note that if you misspell a function argument or want to use one that isn't available, you'll see an error @@ -114,130 +147,517 @@ to see available function arguments. -The request object passed to factories ------------------------------------------ -Each funcarg factory receives a :py:class:`~_pytest.python.FuncargRequest` 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. +Location independency of funcarg factories +---------------------------------------------------- +If during implementing your tests you realize that you +want to use a factory from multiple test files you can move it +to a :ref:`conftest.py ` file or even separately installable +:ref:`plugins ` without changing test code. The discovery of +funcarg factories starts at test classes, then test modules, then +``conftest.py`` files and finally builtin and 3-rd party plugins. -.. _`parametrizing tests, generalized`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/ +Creating and using a session-shared funcarg +----------------------------------------------------------------- -.. _`blog post about the monkeypatch funcarg`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ -.. _`xUnit style`: xunit_setup.html +.. regendoc:wipe +.. versionadded:: 2.3 -.. _`funcarg factory`: -.. _factory: +The `@pytest.factory`_ marker allows to + +* mark a function as a factory for resources, useable by test and setup + functions + +* define parameters in order to run tests multiple times with + different resource instances + +* declare a scope which determines the level of caching, i.e. how often + the factory will be called. Valid scopes are ``session``, ``module``, + ``class`` and ``function``. + +Here is a simple example of a factory creating a shared ``smtplib.SMTP`` +connection resource which test functions then may use across the whole +test session:: + + # content of conftest.py + import pytest + import smtplib + + @pytest.factory(scope="session") + def smtp(testcontext): + return smtplib.SMTP("merlinux.eu") + +The name of the factory is ``smtp`` (the factory function name) +and you can access its result by listing the name ``smtp`` as +an input parameter in any test or setup function:: + + # content of test_module.py + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + assert "merlinux" in response[1] + assert 0 # for demo purposes + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + assert 0 # for demo purposes + +We deliberately insert failing ``assert 0`` statements in order to +inspect what is going on and can now run the tests:: + + $ py.test -q test_module.py + collecting ... collected 2 items + FF + ================================= FAILURES ================================= + ________________________________ test_ehlo _________________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + assert "merlinux" in response[1] + > assert 0 # for demo purposes + E assert 0 + + test_module.py:5: AssertionError + ________________________________ test_noop _________________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + 2 failed in 0.20 seconds + +you see the two ``assert 0`` failing and can also see that +the same (session-scoped) object was passed into the two test functions. + + +Parametrizing a session-shared funcarg resource +----------------------------------------------------------------- + +Extending the previous example, we can flag the factory to create +two ``smtp`` values which will cause all tests using it to +run twice with two different values. The factory function gets +access to each parameter through the special `testcontext`_ object:: + + # content of conftest.py + import pytest + import smtplib + + @pytest.factory(scope="session", + params=["merlinux.eu", "mail.python.org"]) + def smtp(testcontext): + return smtplib.SMTP(testcontext.param) + +The main change is the definition of a ``params`` list in the +``factory``-marker and the ``testcontext.param`` access within the +factory function. No test function code needs to change. +So let's just do another run:: + + $ py.test -q + collecting ... collected 4 items + FFFF + ================================= FAILURES ================================= + __________________________ test_ehlo[merlinux.eu] __________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + assert "merlinux" in response[1] + > assert 0 # for demo purposes + E assert 0 + + test_module.py:5: AssertionError + __________________________ test_noop[merlinux.eu] __________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + ________________________ test_ehlo[mail.python.org] ________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + > assert "merlinux" in response[1] + E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + + test_module.py:4: AssertionError + ________________________ test_noop[mail.python.org] ________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + 4 failed in 6.00 seconds + +We get four failures because we are running the two tests twice with +different ``smtp`` instantiations as defined on the factory. +Note that with the ``mail.python.org`` connection the second test +fails in ``test_ehlo`` because it expects a specific server string. + +Adding a finalizer to the parametrized resource +-------------------------------------------------------- + +Further extending the ``smtp`` example, we now want to properly +close a smtp server connection after the last test using it +has been run. We can do this by calling the ``testcontext.addfinalizer()`` +helper:: + + # content of conftest.py + import pytest + import smtplib + + @pytest.factory(scope="session", + params=["merlinux.eu", "mail.python.org"]) + def smtp(testcontext): + smtp = smtplib.SMTP(testcontext.param) + def fin(): + print ("finalizing %s" % smtp) + smtp.close() + testcontext.addfinalizer(fin) + return smtp + +We also add a print call and then run py.test without the default +output capturing and disabled traceback reporting:: + + $ py.test -s -q --tb=no + collecting ... collected 4 items + FFFF + 4 failed in 5.62 seconds + finalizing + finalizing + +We see that the two ``smtp`` instances are finalized appropriately. + +Looking at test collection without running tests +------------------------------------------------------ + +You can also look at what tests pytest collects without running them:: + + $ py.test --collectonly + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + collecting ... collected 4 items + + + + + + + ============================= in 0.02 seconds ============================= + +Note that pytest orders your test run by resource usage, minimizing +the number of active resources at any given time. + + +.. _`interdependent resources`: + +Interdepdendent resources +---------------------------------------------------------- + +You can not only use funcargs in test functions but also in their factories +themselves. Extending the previous example we can instantiate another +object ``app`` and stick the ``smtp`` resource into it like this:: + + # content of test_appsetup.py + + import pytest + + class App: + def __init__(self, smtp): + self.smtp = smtp + + @pytest.factory(scope="module") + def app(smtp): + return App(smtp) + + def test_exists(app): + assert app.smtp + +Here we define the factory local to the test module and make it access +the ``smtp`` resource by listing it as an input parameter. Let's run this:: + + $ py.test -v test_appsetup.py + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-414/.cache + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + collecting ... collected 2 items + + test_appsetup.py:12: test_exists[merlinux.eu] PASSED + test_appsetup.py:12: test_exists[mail.python.org] PASSED + + ========================= 2 passed in 5.37 seconds ========================= + +Due to the parametrization of ``smtp`` the test will run twice with two +different ``App`` instances and respective smtp servers. There is no +need for the ``app`` factory to be aware of the parametrization. Note +that the ``app`` factory has a scope of ``module`` whereas it uses the +session-scoped ``smtp`` object: it is fine for factories to use +"broader" scoped resources but not the other way round. A +session-scoped resource could not use a module-scoped resource in a +meaningful way. + +.. _`automatic per-resource grouping`: + +Grouping tests by resource parameters +---------------------------------------------------------- + +.. regendoc: wipe + +pytest minimizes the number of active resources during test runs. +If you have a parametrized resource, then all the tests using one +resource instance will execute one after another. Then any finalizers +are called for that resource instance and then the next parametrized +resource instance is created and its tests are run. Among other things, +this eases testing of applications which create and use global state. + +The following example uses two parametrized funcargs, one of which is +scoped on a per-module basis, and all the functions perform ``print`` call +it to document the flow of calls:: + + # content of test_module.py + import pytest + + @pytest.factory(scope="module", params=["mod1", "mod2"]) + def modarg(testcontext): + param = testcontext.param + print "create", param + def fin(): + print "fin", param + testcontext.addfinalizer(fin) + return param + + @pytest.factory(scope="function", params=[1,2]) + def otherarg(testcontext): + return testcontext.param + + def test_0(otherarg): + print " test0", otherarg + def test_1(modarg): + print " test1", modarg + def test_2(otherarg, modarg): + print " test2", otherarg, modarg + +Let's run the tests in verbose mode and with looking at the print-output:: + + $ py.test -v -s test_module.py + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-414/.cache + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + collecting ... collected 8 items + + test_module.py:16: test_0[1] PASSED + test_module.py:16: test_0[2] PASSED + test_module.py:18: test_1[mod1] PASSED + test_module.py:20: test_2[1-mod1] PASSED + test_module.py:20: test_2[2-mod1] PASSED + test_module.py:18: test_1[mod2] PASSED + test_module.py:20: test_2[1-mod2] PASSED + test_module.py:20: test_2[2-mod2] PASSED + + ========================= 8 passed in 0.02 seconds ========================= + test0 1 + test0 2 + create mod1 + test1 mod1 + test2 1 mod1 + test2 2 mod1 + fin mod1 + create mod2 + test1 mod2 + test2 1 mod2 + test2 2 mod2 + fin mod2 + +You can see that the parametrized module-scoped ``modarg`` resource caused +a re-ordering of test execution. The finalizer for the ``mod1`` parametrized +resource was executed before the ``mod2`` resource was setup. + +.. currentmodule:: _pytest.python +.. _`testcontext`: + +``testcontext``: interacting with test context +--------------------------------------------------- + +The ``testcontext`` object may be used by `@pytest.factory`_ or +:ref:`@pytest.setup ` marked functions. It contains +information relating to the test context within which the function +executes. Moreover, you can call +``testcontext.addfinalizer(myfinalizer)`` in order to trigger a call to +``myfinalizer`` after the last test in the test context has executed. +If passed to a parametrized factory ``testcontext.param`` will contain a +parameter (one value out of the ``params`` list specified with the +`@pytest.factory`_ marker). + +.. autoclass:: _pytest.python.TestContext() + :members: + + .. _`test generators`: .. _`parametrizing-tests`: .. _`parametrized test functions`: -Parametrizing multiple calls to a test function -=========================================================== +Parametrizing test functions +========================================================================== -You can parametrize multiple runs of the same test -function by adding new test function calls with different -function argument values. Let's look at a simple self-contained -example: +While the `@pytest.factory`_ decorator allows to define parametrization +of funcarg resources at the factory-level, there are also means to +define parametrization at test functions directly: -Basic generated test example ----------------------------- +* `@pytest.mark.parametrize`_ to provide multiple argument sets + for a particular test function or class. -Let's consider a test module which uses the ``pytest_generate_tests`` -hook to generate several calls to the same test function:: +* `pytest_generate_tests`_ to implement your own custom parametrization + scheme or extensions. - # content of test_example.py +.. _`@pytest.mark.parametrize`: + +``@pytest.mark.parametrize``: parametrizing test functions +--------------------------------------------------------------------- + +.. regendoc: wipe + +.. versionadded:: 2.2 + +The builtin ``pytest.mark.parametrize`` decorator enables +parametrization of arguments for a test function. Here is a typical example +of a test function that wants check for expected output given a certain input:: + + # content of test_expectation.py + import pytest + @pytest.mark.parametrize(("input", "expected"), [ + ("3+5", 8), + ("2+4", 6), + ("6*9", 42), + ]) + def test_eval(input, expected): + assert eval(input) == expected + +The ``@parametrize`` decorator defines three different argument sets for the +two ``(input, output)`` arguments of ``test_eval`` function so the latter +will be run three times:: + + $ py.test -q + + collecting ... collected 13 items + ....F........ + ================================= FAILURES ================================= + ____________________________ test_eval[6*9-42] _____________________________ + + input = '6*9', expected = 42 + + @pytest.mark.parametrize(("input", "expected"), [ + ("3+5", 8), + ("2+4", 6), + ("6*9", 42), + ]) + def test_eval(input, expected): + > assert eval(input) == expected + E assert 54 == 42 + E + where 54 = eval('6*9') + + test_expectation.py:8: AssertionError + 1 failed, 12 passed in 5.76 seconds + +As expected only one pair of input/output values fails the simple test function. +As usual you can see the ``input`` and ``output`` values in the traceback. + +Note that there are various ways how you can mark groups of functions, +see :ref:`mark`. + + +.. _`pytest_generate_tests`: + +Basic ``pytest_generate_tests`` example +--------------------------------------------- + +The ``pytest_generate_tests`` hook is typically used if you want +to go beyond what ``@pytest.mark.parametrize`` offers. For example, +let's say we want to execute a test with different computation +parameters and the parameter range shall be determined by a command +line argument. Let's first write a simple (do-nothing) computation test:: + + # content of test_compute.py + + def test_compute(param1): + assert param1 < 4 + +Now we add a ``conftest.py`` file containing the addition of a +command line option and the generation of tests depending on +that option:: + + # content of conftest.py + + def pytest_addoption(parser): + parser.addoption("--all", action="store_true", + help="run all combinations") + def pytest_generate_tests(metafunc): - if "numiter" in metafunc.funcargnames: - metafunc.parametrize("numiter", range(10)) + if 'param1' in metafunc.funcargnames: + if metafunc.config.option.all: + end = 5 + else: + end = 2 + metafunc.parametrize("param1", range(end)) - def test_func(numiter): - assert numiter < 9 +This means that we only run two tests if no option is passed:: -Running this will generate ten invocations of ``test_func`` passing in each of the items in the list of ``range(10)``:: + $ py.test -q test_compute.py + collecting ... collected 2 items + .. + 2 passed in 0.02 seconds - $ py.test test_example.py - =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 - plugins: xdist, bugzilla, cache, oejskit, pep8, cov - collecting ... collected 10 items +And we run five tests if we add the ``--all`` option:: + + $ py.test -q --all + collecting ... collected 5 items + ....F + ================================= FAILURES ================================= + _____________________________ test_compute[4] ______________________________ - test_example.py .........F + param1 = 4 - ================================= FAILURES ================================= - _______________________________ test_func[9] _______________________________ + def test_compute(param1): + > assert param1 < 4 + E assert 4 < 4 - numiter = 9 - - def test_func(numiter): - > assert numiter < 9 - E assert 9 < 9 - - test_example.py:6: AssertionError - ==================== 1 failed, 9 passed in 0.03 seconds ==================== + test_compute.py:3: AssertionError + 1 failed, 4 passed in 0.03 seconds -Obviously, only when ``numiter`` has the value of ``9`` does the test fail. Note that the ``pytest_generate_tests(metafunc)`` hook is called during -the test collection phase which is separate from the actual test running. -Let's just look at what is collected:: - - $ py.test --collectonly test_example.py - =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 - plugins: xdist, bugzilla, cache, oejskit, pep8, cov - collecting ... collected 10 items - - - - - - - - - - - - - ============================= in 0.02 seconds ============================= - -If you want to select only the run with the value ``7`` you could do:: - - $ py.test -v -k 7 test_example.py # or -k test_func[7] - =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 -- /home/hpk/venv/1/bin/python - cachedir: /home/hpk/tmp/doc-exec-271/.cache - plugins: xdist, bugzilla, cache, oejskit, pep8, cov - collecting ... collected 10 items - - test_example.py:5: test_func[0] PASSED - test_example.py:5: test_func[1] PASSED - test_example.py:5: test_func[2] PASSED - test_example.py:5: test_func[3] PASSED - test_example.py:5: test_func[4] PASSED - test_example.py:5: test_func[5] PASSED - test_example.py:5: test_func[6] PASSED - test_example.py:5: test_func[7] PASSED - test_example.py:5: test_func[8] PASSED - test_example.py:5: test_func[9] FAILED - - ================================= FAILURES ================================= - _______________________________ test_func[9] _______________________________ - - numiter = 9 - - def test_func(numiter): - > assert numiter < 9 - E assert 9 < 9 - - test_example.py:6: AssertionError - ==================== 1 failed, 9 passed in 0.03 seconds ==================== +As expected when running the full range of ``param1`` values +we'll get an error on the last one. You might want to look at :ref:`more parametrization examples `. + .. _`metafunc object`: The **metafunc** object @@ -260,3 +680,52 @@ .. automethod:: Metafunc.parametrize .. automethod:: Metafunc.addcall(funcargs=None,id=_notexists,param=_notexists) + + +.. regendoc:wipe + + +.. _`compatibility notes`: + +.. _`funcargscompat`: + +Compatibility notes +============================================================ + +**Funcargs** were originally introduced to pytest-2.0. In pytest-2.3 +the mechanism were extended and refined. Here are some related notes: + +* previously funcarg factories were specified with a special + ``pytest_funcarg__NAME`` prefix instead of using the + ``@pytest.factory`` decorator. + +* Factories received a `request`_ object which managed caching through + ``request.cached_setup()`` calls and allowed using other funcargs via + ``request.getfuncargvalue()`` calls. These intricate APIs made it hard + to do proper parametrization and implement resource caching. The + new ``@pytest.factory`` decorator allows to simply declare the scope + and let pytest figure things out for you. + +* if you used parametrization and funcarg factories which made use of + ``request.cached_setup()`` it is recommeneded to invest a few minutes + and simplify your funcarg factory code to use the `@pytest.factory`_ + decorator instead. This will also allow to take advantage of + the `automatic per-resource grouping`_ of tests. + +.. note:: + + Throughout the pytest documents the ``pytest_funcarg__NAME`` way of + defining a funcarg factory is often termed "old-style". Their + use remains fully supported and existing code using it should run + unmodified. + +.. _request: + +The request object passed to old-style factories +----------------------------------------------------------------- + +Old-style funcarg factory definitions can receive a :py:class:`~_pytest.python.FuncargRequest` 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. + + diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/index.txt --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -27,7 +27,7 @@ - (new in 2.3) :ref:`easy test resource management, scoping and parametrization ` - - (new in 2.3) :ref:`xunitsetup`. + - (new in 2.3) :ref:`setup functions`. - (new in 2.2) :ref:`durations` - (much improved in 2.2) :ref:`marking and test selection ` - (improved in 2.2) :ref:`parametrized test functions ` diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/resources.txt --- a/doc/en/resources.txt +++ /dev/null @@ -1,487 +0,0 @@ - -.. _resources: - -======================================================= -test resource injection and parametrization -======================================================= - -.. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection - -.. versionadded: 2.3 - -pytest offers very flexible means for managing test resources and -test parametrization. - -The pytest resource management mechanism is an example of `Dependency -Injection`_ because test and :ref:`setup functions ` receive -resources simply by stating them as an input argument. Therefore and -also for historic reasons, they are often called **funcargs**. At test -writing time you typically do not need to care for the details of how -your required resources are constructed, if they live through a -function, class, module or session scope or if the test will be called -multiple times with different resource instances. - -To create a value with which to call a test function a resource factory -function is called which gets full access to the test context and can -register finalizers which are to be run after the last test in that context -finished. Resource factories can be implemented in same test class or -test module, in a per-directory ``conftest.py`` file or in an external -plugin. This allows **total de-coupling of test and setup code**, -lowering the cost of refactoring. - -A test function may be invoked multiple times in which case we -speak of parametrization. You can parametrize resources or parametrize -test function arguments directly or even implement your own parametrization -scheme through a plugin hook. - -A resource has a **name** under which test and setup functions -can access it by listing it as an input argument. Due to this and -also for historic reasons, resources are often called **funcargs**. -A resource is created by a factory which can be flagged with a **scope** -to only create resources on a per-class/per-module/per-session basis -instead of the default per-function scope. - -Concretely, there are three means of resource and parametrization management: - -* a `@pytest.factory`_ marker to define resource factories, - their scoping and parametrization. Factories can themselves - receive resources through their function arguments, easing - the setup of interdependent resources. They can also use - the special `testcontext`_ object to access details n which - the factory/setup is called and for registering finalizers. - -* a `@pytest.mark.parametrize`_ marker for executing test functions - multiple times with different parameter sets - -* a `pytest_generate_tests`_ plugin hook marker for implementing - your parametrization for a test function which may depend on - command line options, class/module attributes etc. - -Finally, pytest comes with some :ref:`builtinresources` which -you can use without defining them yourself. Moreover, third-party -plugins offer their own resources so that after installation -you can simply use them in your test and setup functions. - -.. _`@pytest.factory`: - -``@pytest.factory``: Creating parametrized, scoped resources ------------------------------------------------------------------ - -.. regendoc:wipe - -.. versionadded:: 2.3 - -The `@pytest.factory`_ marker allows to - -* mark a function as a factory for resources, useable by test and setup functions -* define parameters in order to run tests multiple times with different - resource instances -* set a scope which determines the level of caching, i.e. how often - the factory will be called. Valid scopes are ``session``, ``module``, - ``class`` and ``function``. - -Here is a simple example of a factory creating a shared ``smtplib.SMTP`` -connection resource which test functions then may use across the whole -test session:: - - # content of conftest.py - import pytest - import smtplib - - @pytest.factory(scope="session") - def smtp(testcontext): - smtp = smtplib.SMTP("merlinux.eu") - testcontext.addfinalizer(smtp.close) - return smtp - -The name of the resource is ``smtp`` (the factory function name) -and you can now access the ``smtp`` resource by listing it as -an input parameter in any test function below the directory where -``conftest.py`` is located:: - - # content of test_module.py - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - assert "merlinux" in response[1] - assert 0 # for demo purposes - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - assert 0 # for demo purposes - -If you run the tests:: - - $ py.test -q - collecting ... collected 2 items - FF - ================================= FAILURES ================================= - ________________________________ test_ehlo _________________________________ - - smtp = - - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - assert "merlinux" in response[1] - > assert 0 # for demo purposes - E assert 0 - - test_module.py:5: AssertionError - ________________________________ test_noop _________________________________ - - smtp = - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - > assert 0 # for demo purposes - E assert 0 - - test_module.py:10: AssertionError - 2 failed in 0.18 seconds - -you will see the two ``assert 0`` failing and can see that -the same (session-scoped) object was passed into the two test functions. - -If you now want to test multiple servers you can simply parametrize -the ``smtp`` factory:: - - # content of conftest.py - import pytest - import smtplib - - @pytest.factory(scope="session", - params=["merlinux.eu", "mail.python.org"]) - def smtp(testcontext): - smtp = smtplib.SMTP(testcontext.param) - def fin(): - smtp.close() - testcontext.addfinalizer(fin) - return smtp - -The main change is the definition of a ``params`` list in the -``factory``-marker and the ``testcontext.param`` access within the -factory function. No test code needs to change. So let's just do another -run:: - - $ py.test -q - collecting ... collected 4 items - FFFF - ================================= FAILURES ================================= - __________________________ test_ehlo[merlinux.eu] __________________________ - - smtp = - - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - assert "merlinux" in response[1] - > assert 0 # for demo purposes - E assert 0 - - test_module.py:5: AssertionError - __________________________ test_noop[merlinux.eu] __________________________ - - smtp = - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - > assert 0 # for demo purposes - E assert 0 - - test_module.py:10: AssertionError - ________________________ test_ehlo[mail.python.org] ________________________ - - smtp = - - def test_ehlo(smtp): - response = smtp.ehlo() - assert response[0] == 250 - > assert "merlinux" in response[1] - E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' - - test_module.py:4: AssertionError - ________________________ test_noop[mail.python.org] ________________________ - - smtp = - - def test_noop(smtp): - response = smtp.noop() - assert response[0] == 250 - > assert 0 # for demo purposes - E assert 0 - - test_module.py:10: AssertionError - 4 failed in 6.42 seconds - -We get four failures because we are running the two tests twice with -different ``smtp`` instantiations as defined on the factory. -Note that with the ``mail.python.org`` connection the second test -fails in ``test_ehlo`` because it expects a specific server string. - -You can also look at what tests pytest collects without running them:: - - $ py.test --collectonly - =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev7 - plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov - collecting ... collected 4 items - - - - - - - ============================= in 0.02 seconds ============================= - -Note that pytest orders your test run by resource usage, minimizing -the number of active resources at any given time. - - -Interdepdendent resources ----------------------------------------------------------- - -You can not only use resources in test functions but also in resource factories -themselves. Extending the previous example we can instantiate an application -object by sticking the ``smtp`` resource into it:: - - # content of test_appsetup.py - - import pytest - - class App: - def __init__(self, smtp): - self.smtp = smtp - - @pytest.factory(scope="module") - def app(smtp): - return App(smtp) - - def test_exists(app): - assert app.smtp - -Let's run this:: - - $ py.test -v test_appsetup.py - =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev7 -- /home/hpk/venv/1/bin/python - cachedir: /home/hpk/tmp/doc-exec-398/.cache - plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov - collecting ... collected 2 items - - test_appsetup.py:12: test_exists[merlinux.eu] PASSED - test_appsetup.py:12: test_exists[mail.python.org] PASSED - - ========================= 2 passed in 5.96 seconds ========================= - -Due to the parametrization of ``smtp`` the test will -run twice with two different ``App`` instances and respective smtp servers. -There is no need for the ``app`` factory to be aware of the parametrization. - - - -Grouping tests by resource parameters ----------------------------------------------------------- - -.. regendoc: wipe - -pytest minimizes the number of active resources during test runs. -If you have a parametrized resource, then all the tests using one -resource instance will execute one after another. Then any finalizers -are called for that resource instance and then the next parametrized -resource instance is created and its tests are run. Among other things, -this eases testing of applications which create and use global state. - -The following example uses two parametrized funcargs, one of which is -scoped on a per-module basis:: - - # content of test_module.py - import pytest - - @pytest.factory(scope="module", params=["mod1", "mod2"]) - def modarg(testcontext): - param = testcontext.param - print "create", param - def fin(): - print "fin", param - testcontext.addfinalizer(fin) - return param - - @pytest.factory(scope="function", params=[1,2]) - def otherarg(testcontext): - return testcontext.param - - def test_0(otherarg): - print " test0", otherarg - def test_1(modarg): - print " test1", modarg - def test_2(otherarg, modarg): - print " test2", otherarg, modarg - -Let's run the tests in verbose mode and with looking at the print-output:: - - $ py.test -v -s - =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev7 -- /home/hpk/venv/1/bin/python - cachedir: /home/hpk/tmp/doc-exec-398/.cache - plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov - collecting ... collected 8 items - - test_module.py:16: test_0[1] PASSED - test_module.py:16: test_0[2] PASSED - test_module.py:18: test_1[mod1] PASSED - test_module.py:20: test_2[1-mod1] PASSED - test_module.py:20: test_2[2-mod1] PASSED - test_module.py:18: test_1[mod2] PASSED - test_module.py:20: test_2[1-mod2] PASSED - test_module.py:20: test_2[2-mod2] PASSED - - ========================= 8 passed in 0.03 seconds ========================= - test0 1 - test0 2 - create mod1 - test1 mod1 - test2 1 mod1 - test2 2 mod1 - fin mod1 - create mod2 - test1 mod2 - test2 1 mod2 - test2 2 mod2 - fin mod2 - -You can see that the parametrized module-scoped ``modarg`` resource caused -a re-ordering of test execution. The finalizer for the ``mod1`` parametrized -resource was executed before the ``mod2`` resource was setup. - -.. currentmodule:: _pytest.python -.. _`testcontext`: - -``testcontext``: interacting with test context ---------------------------------------------------- - -The ``testcontext`` object may be received by `@pytest.factory`_ or -`@pytest.setup`_ marked functions. It contains information relating -to the test context within which the function executes. Moreover, you -can call ``testcontext.addfinalizer(myfinalizer)`` in order to trigger -a call to ``myfinalizer`` after the last test in the test context has executed. -If passed to a parametrized factory ``testcontext.param`` will contain -a parameter (one value out of the ``params`` list specified with the -`@pytest.factory`_ marker). - -.. autoclass:: _pytest.python.TestContext() - :members: - -.. _`@pytest.mark.parametrize`: - -``@pytest.mark.parametrize``: directly parametrizing test functions ----------------------------------------------------------------------------- - -.. versionadded:: 2.2 - -The builtin ``pytest.mark.parametrize`` decorator enables -parametrization of arguments for a test function. Here is an example -of a test function that wants check for expected output given a certain input:: - - # content of test_expectation.py - import pytest - @pytest.mark.parametrize(("input", "expected"), [ - ("3+5", 8), - ("2+4", 6), - ("6*9", 42), - ]) - def test_eval(input, expected): - assert eval(input) == expected - -we parametrize two arguments of the test function so that the test -function is called three times. Let's run it:: - - $ py.test -q - collecting ... collected 11 items - ..F........ - ================================= FAILURES ================================= - ____________________________ test_eval[6*9-42] _____________________________ - - input = '6*9', expected = 42 - - @pytest.mark.parametrize(("input", "expected"), [ - ("3+5", 8), - ("2+4", 6), - ("6*9", 42), - ]) - def test_eval(input, expected): - > assert eval(input) == expected - E assert 54 == 42 - E + where 54 = eval('6*9') - - test_expectation.py:8: AssertionError - 1 failed, 10 passed in 0.04 seconds - -As expected only one pair of input/output values fails the simple test function. - -Note that there are various ways how you can mark groups of functions, -see :ref:`mark`. - - -.. _`pytest_generate_tests`: - -``pytest_generate_test``: implementing your own parametrization scheme ----------------------------------------------------------------------------- - -.. regendoc:wipe - -Let's say we want to execute a test with different computation -parameters and the parameter range shall be determined by a command -line argument. Let's first write a simple (do-nothing) computation test:: - - # content of test_compute.py - - def test_compute(param1): - assert param1 < 4 - -Now we add a test configuration like this:: - - # content of conftest.py - - def pytest_addoption(parser): - parser.addoption("--all", action="store_true", - help="run all combinations") - - def pytest_generate_tests(metafunc): - if 'param1' in metafunc.funcargnames: - if metafunc.config.option.all: - end = 5 - else: - end = 2 - metafunc.parametrize("param1", range(end)) - -This means that we only run 2 tests if we do not pass ``--all``:: - - $ py.test -q test_compute.py - collecting ... collected 2 items - .. - 2 passed in 0.03 seconds - -We run only two computations, so we see two dots. -let's run the full monty:: - - $ py.test -q --all - collecting ... collected 5 items - ....F - ================================= FAILURES ================================= - _____________________________ test_compute[4] ______________________________ - - param1 = 4 - - def test_compute(param1): - > assert param1 < 4 - E assert 4 < 4 - - test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.03 seconds - -As expected when running the full range of ``param1`` values -we'll get an error on the last one. diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/setup.txt --- a/doc/en/setup.txt +++ b/doc/en/setup.txt @@ -1,5 +1,7 @@ .. _xunitsetup: .. _setup: +.. _`setup functions`: +.. _`@pytest.setup`: ``@setup`` functions or: xunit on steroids ======================================================== @@ -18,19 +20,20 @@ functions work indirectly by causing global side effects or setting test case attributes which test methods can then access. -pytest originally introduced in 2005 a fine-grained model of detecting +pytest originally introduced in 2005 a scope-specific model of detecting setup and teardown functions on a per-module, class or function basis. -The Python unittest module and nose have subsequently incorporated them. -This model remains supported as :ref:`old-style xunit`. +The Python unittest package and nose have subsequently incorporated them. +This model remains supported by pytest as :ref:`old-style xunit`. -With pytest-2.3 a new ``pytest.setup()`` decorator is introduced -to mark functions as setup functions which: +Moreover, pytest-2.3 introduces a new ``pytest.setup()`` decorator +to mark functions as setup functions which allow to implement everything +you can do with the old-style and much more. Specifically setup functions: -- can receive resources through funcargs, +- can receive :ref:`resources through funcargs `, - fully interoperate with parametrized resources, -- can be defined in a plugin or conftest.py file and get called +- can be defined in a plugin or :ref:`conftest.py` file and get called on a per-session, per-module, per-class or per-function basis, -- can access the full :ref:`testcontext` for which the setup is called, +- can access the :ref:`testcontext ` for which the setup is called, - can precisely control teardown by registering one or multiple teardown functions as soon as they have performed some actions which need undoing, eliminating the no need for a separate @@ -75,7 +78,7 @@ the default scope used by the ``pytest.setup`` decorator. It accesses the ``tmpdir`` resource which provides a new empty directory path object. The ``test_function2`` here checks that -it executes with a fresh directory and specifically +it executes with a fresh directory and that it does not see the previously created ``anotherfile``. We can thus expect two passing tests:: @@ -115,6 +118,11 @@ test modules accessing a global resource ------------------------------------------------------- +.. note:: + + Relying on `global state is considered bad programming practise `_ but when you work with an application + that relies on it you often have no choice. + If you want test modules to access a global resource, you can stick the resource to the module globals in a per-module setup function. We use a :ref:`resource factory diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/xunit_old.txt --- a/doc/en/xunit_old.txt +++ /dev/null @@ -1,76 +0,0 @@ - -.. _`old-style xunit`: - -Old-style xunit-style setup -======================================== - -This section describes the old way how you can implement setup and -teardown on a per-module/class/function basis. It remains fully -supported but it is recommended to rather use :ref:`@setup functions ` -or :ref:`injected resources ` for implementing your setup needs. - -Module level setup/teardown --------------------------------------- - -If you have multiple test functions and test classes in a single -module you can optionally implement the following fixture methods -which will usually be called once for all the functions:: - - def setup_module(module): - """ setup any state specific to the execution of the given module.""" - - def teardown_module(module): - """ teardown any state that was previously setup with a setup_module - method. - """ - -Class level setup/teardown ----------------------------------- - -Similarly, the following methods are called at class level before -and after all test methods of the class are called:: - - @classmethod - def setup_class(cls): - """ setup any state specific to the execution of the given class (which - usually contains tests). - """ - - @classmethod - def teardown_class(cls): - """ teardown any state that was previously setup with a call to - setup_class. - """ - -Method and function level setup/teardown ------------------------------------------------ - -Similarly, the following methods are called around each method invocation:: - - def setup_method(self, method): - """ setup any state tied to the execution of the given method in a - class. setup_method is invoked for every test method of a class. - """ - - def teardown_method(self, method): - """ teardown any state that was previously setup with a setup_method - call. - """ - -If you would rather define test functions directly at module level -you can also use the following functions to implement fixtures:: - - def setup_function(function): - """ setup any state tied to the execution of the given function. - Invoked for every test function in the module. - """ - - def teardown_function(function): - """ teardown any state that was previously setup with a setup_function - call. - """ - -Note that it is possible for setup/teardown pairs to be invoked multiple times -per testing process. - -.. _`unittest.py module`: http://docs.python.org/library/unittest.html diff -r e811b2a6819856b7db636f3bff7a7510164f2d97 -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 doc/en/xunit_setup.txt --- /dev/null +++ b/doc/en/xunit_setup.txt @@ -0,0 +1,79 @@ + +.. _`old-style xunit`: + +Old-style xunit-style setup +======================================== + +.. note:: + + This section describes the old way how you can implement setup and + teardown on a per-module/class/function basis. It remains fully + supported but it is recommended to rather use :ref:`@setup functions + ` or :ref:`injected resources ` for implementing your + setup needs. + +Module level setup/teardown +-------------------------------------- + +If you have multiple test functions and test classes in a single +module you can optionally implement the following fixture methods +which will usually be called once for all the functions:: + + def setup_module(module): + """ setup any state specific to the execution of the given module.""" + + def teardown_module(module): + """ teardown any state that was previously setup with a setup_module + method. + """ + +Class level setup/teardown +---------------------------------- + +Similarly, the following methods are called at class level before +and after all test methods of the class are called:: + + @classmethod + def setup_class(cls): + """ setup any state specific to the execution of the given class (which + usually contains tests). + """ + + @classmethod + def teardown_class(cls): + """ teardown any state that was previously setup with a call to + setup_class. + """ + +Method and function level setup/teardown +----------------------------------------------- + +Similarly, the following methods are called around each method invocation:: + + def setup_method(self, method): + """ setup any state tied to the execution of the given method in a + class. setup_method is invoked for every test method of a class. + """ + + def teardown_method(self, method): + """ teardown any state that was previously setup with a setup_method + call. + """ + +If you would rather define test functions directly at module level +you can also use the following functions to implement fixtures:: + + def setup_function(function): + """ setup any state tied to the execution of the given function. + Invoked for every test function in the module. + """ + + def teardown_function(function): + """ teardown any state that was previously setup with a setup_function + call. + """ + +Note that it is possible for setup/teardown pairs to be invoked multiple times +per testing process. + +.. _`unittest.py module`: http://docs.python.org/library/unittest.html Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Aug 4 09:55:00 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 04 Aug 2012 07:55:00 -0000 Subject: [py-svn] commit/pytest-xdist: hpk42: improve README to cover --boxed option and contain some more links Message-ID: <20120804075500.3290.93810@bitbucket13.managed.contegix.com> 1 new commit in pytest-xdist: https://bitbucket.org/hpk42/pytest-xdist/changeset/99549e737f22/ changeset: 99549e737f22 user: hpk42 date: 2012-08-04 09:54:51 summary: improve README to cover --boxed option and contain some more links affected #: 1 file diff -r d8af4f85360aae594450fab79d59a2d9e09c4dc8 -r 99549e737f220f58e57d8cb1a107370f15379568 README.txt --- a/README.txt +++ b/README.txt @@ -4,16 +4,19 @@ The `pytest-xdist`_ plugin extends py.test with some unique test execution modes: -* Looponfail: run your tests repeatedly in a subprocess. After each run py.test - waits until a file in your project changes and then re-runs the previously - failing tests. This is repeated until all tests pass after which again - a full run is performed. +* test run parallelization_: if you have multiple CPUs or hosts you can use + those for a combined test run. This allows to speed up + development or to use special resources of `remote machines`_. -* multiprocess Load-balancing: if you have multiple CPUs or hosts you can use - those for a combined test run. This allows to speed up - development or to use special resources of remote machines. +* ``--boxed``: run each test in a boxed_ subprocess to survive ``SEGFAULTS`` or + otherwise dying processes -* Multi-Platform coverage: you can specify different Python interpreters +* ``--looponfail``: run your tests repeatedly in a subprocess. After each run + py.test waits until a file in your project changes and then re-runs + the previously failing tests. This is repeated until all tests pass + after which again a full run is performed. + +* `Multi-Platform`_ coverage: you can specify different Python interpreters or different platforms and run tests in parallel on all of them. Before running tests remotely, ``py.test`` efficiently "rsyncs" your @@ -41,6 +44,8 @@ Usage examples --------------------- +.. _parallelization: + Speed up test runs by sending tests to multiple CPUs +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -55,20 +60,42 @@ Running tests in a Python subprocess +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -To instantiate a python2.4 sub process and send tests to it, you may type:: +To instantiate a python2.5 sub process and send tests to it, you may type:: - py.test -d --tx popen//python=python2.4 + py.test -d --tx popen//python=python2.5 -This will start a subprocess which is run with the "python2.4" +This will start a subprocess which is run with the "python2.5" Python interpreter, found in your system binary lookup path. If you prefix the --tx option value like this:: - --tx 3*popen//python=python2.4 + --tx 3*popen//python=python2.5 then three subprocesses would be created and tests will be load-balanced across these three processes. +.. _boxed: + +Running tests in a boxed subprocess ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +If you have tests involving C or C++ libraries you might have to deal +with tests crashing the process. For this case you max use the boxing +options:: + + py.test --boxed + +which will run each test in a subprocess and will report if a test +crashed the process. You can also combine this option with +running multiple processes to speed up the test run and use your CPU cores:: + + py.test -n3 --boxed + +this would run 3 testing subprocesses in parallel which each +create new boxed subprocesses for each test. + + +.. _`remote machines`: Sending tests to remote SSH accounts +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -111,6 +138,8 @@ .. _`atonce`: +.. _`Multi-Platform`: + Running tests on many platforms at once +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Repository URL: https://bitbucket.org/hpk42/pytest-xdist/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Aug 4 10:33:53 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 04 Aug 2012 08:33:53 -0000 Subject: [py-svn] commit/pytest: hpk42: fix/consolidate --junitxml=path construction with relative pathes Message-ID: <20120804083353.22039.38953@bitbucket15.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/8573473debb9/ changeset: 8573473debb9 user: hpk42 date: 2012-08-04 10:33:43 summary: fix/consolidate --junitxml=path construction with relative pathes affected #: 6 files diff -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ Changes between 2.2.4 and 2.3.0.dev ----------------------------------- +- fix junitxml=path construction so that if tests change the + current working directory and the path is a relative path + it is constructed correctly from the original current working dir. - fix "python setup.py test" example to cause a proper "errno" return - fix issue165 - fix broken doc links and mention stackoverflow for FAQ - fix issue139 - merge FuncargRequest and Item API such that diff -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.0.dev8' +__version__ = '2.3.0.dev9' diff -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a _pytest/junitxml.py --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -89,7 +89,7 @@ class LogXML(object): def __init__(self, logfile, prefix): logfile = os.path.expanduser(os.path.expandvars(logfile)) - self.logfile = os.path.normpath(logfile) + self.logfile = os.path.normpath(os.path.abspath(logfile)) self.prefix = prefix self.tests = [] self.passed = self.skipped = 0 diff -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a doc/en/setup.txt --- a/doc/en/setup.txt +++ b/doc/en/setup.txt @@ -31,7 +31,7 @@ - can receive :ref:`resources through funcargs `, - fully interoperate with parametrized resources, -- can be defined in a plugin or :ref:`conftest.py` file and get called +- can be defined in a plugin or :ref:`conftest.py ` file and get called on a per-session, per-module, per-class or per-function basis, - can access the :ref:`testcontext ` for which the setup is called, - can precisely control teardown by registering one or multiple diff -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a setup.py --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.0.dev8', + version='2.3.0.dev9', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r 49dfa381fa5972286b585a1a4dbe0a5b5c100223 -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a testing/test_junitxml.py --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -392,3 +392,15 @@ xml_var = LogXML('$HOME/test.xml', None) assert xml_var.logfile == home_var + +def test_logxml_changingdir(testdir): + testdir.makepyfile(""" + def test_func(): + import os + os.chdir("a") + """) + testdir.tmpdir.mkdir("a") + result = testdir.runpytest("--junitxml=a/x.xml") + assert result.ret == 0 + assert testdir.tmpdir.join("a/x.xml").check() + Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Aug 8 11:49:15 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 08 Aug 2012 09:49:15 -0000 Subject: [py-svn] commit/pytest: hpk42: improve error representation for missing factory definitions Message-ID: <20120808094915.20664.3678@bitbucket12.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/fa6a843aa98b/ changeset: fa6a843aa98b user: hpk42 date: 2012-08-08 11:48:53 summary: improve error representation for missing factory definitions in recursive funcarg reconstruction affected #: 5 files diff -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a -r fa6a843aa98b1f8e9e95aa3b73c0bb4fcfc2e133 _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.0.dev9' +__version__ = '2.3.0.dev10' diff -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a -r fa6a843aa98b1f8e9e95aa3b73c0bb4fcfc2e133 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -285,6 +285,7 @@ fm = self.session.funcargmanager if excinfo.errisinstance(fm.FuncargLookupError): function = excinfo.value.function + factblines = excinfo.value.factblines if function is not None: fspath, lineno = getfslineno(function) lines, _ = inspect.getsourcelines(function) @@ -292,7 +293,7 @@ if line.strip().startswith('def'): return fm.FuncargLookupErrorRepr(fspath, lineno, lines[:i+1], - str(excinfo.value.msg)) + str(excinfo.value.msg), factblines) if self.config.option.fulltrace: style="long" else: diff -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a -r fa6a843aa98b1f8e9e95aa3b73c0bb4fcfc2e133 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -935,17 +935,23 @@ def _getfaclist(self, argname): facdeflist = self._name2factory.get(argname, None) if facdeflist is None: + if self._factorystack: + function = self._factorystack[-1].func + getfactb = lambda: self._factorystack[:-1] + else: + function = self.function + getfactb = None facdeflist = self.funcargmanager.getfactorylist( - argname, self.parentid, self.function) + argname, self.parentid, function, getfactb) self._name2factory[argname] = facdeflist elif not facdeflist: self.funcargmanager._raiselookupfailed(argname, self.function, self.parentid) return facdeflist - def raiseerror(self, msg): - """ raise a FuncargLookupError with the given message. """ - raise self.funcargmanager.FuncargLookupError(self.function, msg) + #def raiseerror(self, msg): + # """ raise a FuncargLookupError with the given message. """ + # raise self.funcargmanager.FuncargLookupError(self.function, msg) @property def function(self): @@ -1102,14 +1108,7 @@ __tracebackhide__ = True if scopemismatch(self.scope, scope): # try to report something helpful - lines = [] - for factorydef in self._factorystack: - factory = factorydef.func - fs, lineno = getfslineno(factory) - p = self._pyfuncitem.session.fspath.bestrelpath(fs) - args = inspect.formatargspec(*inspect.getargspec(factory)) - lines.append("%s:%d\n def %s%s" %( - p, lineno, factory.__name__, args)) + lines = self._factorytraceback() raise ScopeMismatchError("You tried to access the %r scoped " "funcarg %r with a %r scoped request object, " "involved factories\n%s" %( @@ -1129,6 +1128,18 @@ self._funcargs[argname] = val return val + def _factorytraceback(self): + lines = [] + for factorydef in self._factorystack: + factory = factorydef.func + fs, lineno = getfslineno(factory) + p = self._pyfuncitem.session.fspath.bestrelpath(fs) + args = inspect.formatargspec(*inspect.getargspec(factory)) + lines.append("%s:%d: def %s%s" %( + p, lineno, factory.__name__, args)) + return lines + + def _getscopeitem(self, scope): if scope == "function": return self._pyfuncitem @@ -1178,19 +1189,23 @@ class FuncargLookupError(LookupError): """ could not find a factory. """ - def __init__(self, function, msg): + def __init__(self, function, msg, factblines=None): self.function = function self.msg = msg + self.factblines = factblines class FuncargLookupErrorRepr(TerminalRepr): - def __init__(self, filename, firstlineno, deflines, errorstring): + def __init__(self, filename, firstlineno, deflines, errorstring, factblines): self.deflines = deflines self.errorstring = errorstring self.filename = filename self.firstlineno = firstlineno + self.factblines = factblines def toterminal(self, tw): tw.line() + for line in self.factblines or []: + tw.line(line) for line in self.deflines: tw.line(" " + line.strip()) for line in self.errorstring.split("\n"): @@ -1326,12 +1341,12 @@ return l, allargnames - def getfactorylist(self, argname, nodeid, function, raising=True): + def getfactorylist(self, argname, nodeid, function, getfactb=None, raising=True): try: factorydeflist = self.arg2facspec[argname] except KeyError: if raising: - self._raiselookupfailed(argname, function, nodeid) + self._raiselookupfailed(argname, function, nodeid, getfactb) else: return self._matchfactories(factorydeflist, nodeid) @@ -1343,7 +1358,7 @@ l.append(factorydef) return l - def _raiselookupfailed(self, argname, function, nodeid): + def _raiselookupfailed(self, argname, function, nodeid, getfactb=None): available = [] for name, facdef in self.arg2facspec.items(): faclist = self._matchfactories(facdef, nodeid) @@ -1352,7 +1367,8 @@ msg = "LookupError: no factory found for argument %r" % (argname,) msg += "\n available funcargs: %s" %(", ".join(available),) msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise FuncargLookupError(function, msg) + lines = getfactb and getfactb() or [] + raise FuncargLookupError(function, msg, lines) def ensure_setupcalls(self, request): setuplist, allnames = self.getsetuplist(request._pyfuncitem.nodeid) diff -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a -r fa6a843aa98b1f8e9e95aa3b73c0bb4fcfc2e133 setup.py --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.0.dev9', + version='2.3.0.dev10', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r 8573473debb90231fb7fbcb4997ec4e0c7b1cc4a -r fa6a843aa98b1f8e9e95aa3b73c0bb4fcfc2e133 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -2319,7 +2319,7 @@ reprec = testdir.inline_run() reprec.assertoutcome(passed=1) - def test_resource(self, testdir, scope, ok, error): + def test_funcarg(self, testdir, scope, ok, error): testdir.makepyfile(""" import pytest @pytest.factory(scope=%r) @@ -2337,6 +2337,23 @@ reprec = testdir.inline_run() reprec.assertoutcome(passed=1) + +def test_illdefined_factory(testdir): + testdir.makepyfile(""" + import pytest + @pytest.factory() + def gen(request): + return 1 + def test_something(gen): + pass + """) + result = testdir.runpytest() + assert result.ret != 0 + result.stdout.fnmatch_lines([ + "*def gen(request):*", + "*no factory*request*", + ]) + class TestTestContextVarious: def test_newstyle_no_request(self, testdir): testdir.makepyfile(""" Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sat Aug 11 20:02:48 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 11 Aug 2012 18:02:48 -0000 Subject: [py-svn] commit/pytest: 2 new changesets Message-ID: <20120811180248.7373.73541@bitbucket12.managed.contegix.com> 2 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/194f44a84b46/ changeset: 194f44a84b46 user: hpk42 date: 2012-08-08 14:53:47 summary: also improve missing funcarg error for setup functions affected #: 2 files diff -r fa6a843aa98b1f8e9e95aa3b73c0bb4fcfc2e133 -r 194f44a84b4681e1c45c7dd1b8dcf0e70d7a2e86 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -949,9 +949,9 @@ self.parentid) return facdeflist - #def raiseerror(self, msg): - # """ raise a FuncargLookupError with the given message. """ - # raise self.funcargmanager.FuncargLookupError(self.function, msg) + def raiseerror(self, msg): + """ raise a FuncargLookupError with the given message. """ + raise self.funcargmanager.FuncargLookupError(self.function, msg) @property def function(self): @@ -1375,22 +1375,26 @@ for setupcall in setuplist: if setupcall.active: continue - testcontext = TestContextSetup(request, setupcall) - kwargs = {} - for name in setupcall.funcargnames: - try: - kwargs[name] = request.getfuncargvalue(name) - except FuncargLookupError: - if name == "testcontext": - kwargs[name] = testcontext - else: - raise - scope = setupcall.scope or "function" - scol = setupcall.scopeitem = request._getscopeitem(scope) - self.session._setupstate.addfinalizer(setupcall.finish, scol) - for argname in setupcall.funcargnames: # XXX all deps? - self.addargfinalizer(setupcall.finish, argname) - setupcall.execute(kwargs) + request._factorystack.append(setupcall) + try: + testcontext = TestContextSetup(request, setupcall) + kwargs = {} + for name in setupcall.funcargnames: + try: + kwargs[name] = request.getfuncargvalue(name) + except FuncargLookupError: + if name == "testcontext": + kwargs[name] = testcontext + else: + raise + scope = setupcall.scope or "function" + scol = setupcall.scopeitem = request._getscopeitem(scope) + self.session._setupstate.addfinalizer(setupcall.finish, scol) + for argname in setupcall.funcargnames: # XXX all deps? + self.addargfinalizer(setupcall.finish, argname) + setupcall.execute(kwargs) + finally: + request._factorystack.remove(setupcall) def addargfinalizer(self, finalizer, argname): l = self._arg2finish.setdefault(argname, []) diff -r fa6a843aa98b1f8e9e95aa3b73c0bb4fcfc2e133 -r 194f44a84b4681e1c45c7dd1b8dcf0e70d7a2e86 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -2338,21 +2338,41 @@ reprec.assertoutcome(passed=1) -def test_illdefined_factory(testdir): - testdir.makepyfile(""" - import pytest - @pytest.factory() - def gen(request): - return 1 - def test_something(gen): - pass - """) - result = testdir.runpytest() - assert result.ret != 0 - result.stdout.fnmatch_lines([ - "*def gen(request):*", - "*no factory*request*", - ]) +class TestErrors: + def test_subfactory_missing_funcarg(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.factory() + def gen(request): + return 1 + def test_something(gen): + pass + """) + result = testdir.runpytest() + assert result.ret != 0 + result.stdout.fnmatch_lines([ + "*def gen(request):*", + "*no factory*request*", + "*1 error*", + ]) + + def test_setupfunc_missing_funcarg(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.setup() + def gen(request): + return 1 + def test_something(): + pass + """) + result = testdir.runpytest() + assert result.ret != 0 + result.stdout.fnmatch_lines([ + "*def gen(request):*", + "*no factory*request*", + "*1 error*", + ]) + class TestTestContextVarious: def test_newstyle_no_request(self, testdir): https://bitbucket.org/hpk42/pytest/changeset/90a3b3d49ceb/ changeset: 90a3b3d49ceb user: hpk42 date: 2012-08-11 20:02:34 summary: fix typos affected #: 1 file diff -r 194f44a84b4681e1c45c7dd1b8dcf0e70d7a2e86 -r 90a3b3d49cebb9b6484251b7c7f6df1c317305d9 doc/en/funcargs.txt --- a/doc/en/funcargs.txt +++ b/doc/en/funcargs.txt @@ -438,8 +438,8 @@ this eases testing of applications which create and use global state. The following example uses two parametrized funcargs, one of which is -scoped on a per-module basis, and all the functions perform ``print`` call -it to document the flow of calls:: +scoped on a per-module basis, and all the functions perform ``print`` call s +to show the flow of calls:: # content of test_module.py import pytest @@ -508,8 +508,8 @@ The ``testcontext`` object may be used by `@pytest.factory`_ or :ref:`@pytest.setup ` marked functions. It contains -information relating to the test context within which the function -executes. Moreover, you can call +information relating to the test context within which the marked +function executes. Moreover, you can call ``testcontext.addfinalizer(myfinalizer)`` in order to trigger a call to ``myfinalizer`` after the last test in the test context has executed. If passed to a parametrized factory ``testcontext.param`` will contain a @@ -693,7 +693,7 @@ ============================================================ **Funcargs** were originally introduced to pytest-2.0. In pytest-2.3 -the mechanism were extended and refined. Here are some related notes: +the mechanism was extended and refined: * previously funcarg factories were specified with a special ``pytest_funcarg__NAME`` prefix instead of using the Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Mon Aug 13 13:37:48 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Mon, 13 Aug 2012 11:37:48 -0000 Subject: [py-svn] commit/pytest: 2 new changesets Message-ID: <20120813113748.4582.49526@bitbucket16.managed.contegix.com> 2 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/4d9f48622c63/ changeset: 4d9f48622c63 user: hpk42 date: 2012-08-13 12:58:08 summary: fix/update some docs to work with @pytest.factory instead of pytest_funcarg__ naming. affected #: 4 files diff -r 90a3b3d49cebb9b6484251b7c7f6df1c317305d9 -r 4d9f48622c63733d4d67b436d40bdb14fb1ca037 doc/en/example/costlysetup/conftest.py --- a/doc/en/example/costlysetup/conftest.py +++ b/doc/en/example/costlysetup/conftest.py @@ -1,10 +1,11 @@ -def pytest_funcarg__setup(request): - return request.cached_setup( - setup=lambda: CostlySetup(), - teardown=lambda costlysetup: costlysetup.finalize(), - scope="session", - ) +import pytest + + at pytest.factory("session") +def setup(testcontext): + setup = CostlySetup() + testcontext.addfinalizer(setup.finalize) + return setup class CostlySetup: def __init__(self): diff -r 90a3b3d49cebb9b6484251b7c7f6df1c317305d9 -r 4d9f48622c63733d4d67b436d40bdb14fb1ca037 doc/en/example/multipython.py --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -5,29 +5,14 @@ import py, pytest pythonlist = ['python2.4', 'python2.5', 'python2.6', 'python2.7', 'python2.8'] + at pytest.factory(params=pythonlist) +def python1(testcontext, tmpdir): + picklefile = tmpdir.join("data.pickle") + return Python(testcontext.param, picklefile) -def pytest_generate_tests(metafunc): - # we parametrize all "python1" and "python2" arguments to iterate - # over the python interpreters of our list above - the actual - # setup and lookup of interpreters in the python1/python2 factories - # respectively. - for arg in metafunc.funcargnames: - if arg in ("python1", "python2"): - metafunc.parametrize(arg, pythonlist, indirect=True) - - at pytest.mark.parametrize("obj", [42, {}, {1:3},]) -def test_basic_objects(python1, python2, obj): - python1.dumps(obj) - python2.load_and_is_true("obj == %s" % obj) - -def pytest_funcarg__python1(request): - tmpdir = request.getfuncargvalue("tmpdir") - picklefile = tmpdir.join("data.pickle") - return Python(request.param, picklefile) - -def pytest_funcarg__python2(request): - python1 = request.getfuncargvalue("python1") - return Python(request.param, python1.picklefile) + at pytest.factory(params=pythonlist) +def python2(testcontext, python1): + return Python(testcontext.param, python1.picklefile) class Python: def __init__(self, version, picklefile): @@ -58,3 +43,8 @@ """ % (str(self.picklefile), expression))) print (loadfile) py.process.cmdexec("%s %s" %(self.pythonpath, loadfile)) + + at pytest.mark.parametrize("obj", [42, {}, {1:3},]) +def test_basic_objects(python1, python2, obj): + python1.dumps(obj) + python2.load_and_is_true("obj == %s" % obj) diff -r 90a3b3d49cebb9b6484251b7c7f6df1c317305d9 -r 4d9f48622c63733d4d67b436d40bdb14fb1ca037 doc/en/faq.txt --- a/doc/en/faq.txt +++ b/doc/en/faq.txt @@ -123,6 +123,11 @@ source code and safely find all factory functions for the ``MYARG`` function argument. +.. note:: + + With pytest-2.3 you can use the :ref:`@pytest.factory` decorator + to mark a function as a funcarg factory. + .. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration Can I yield multiple values from a funcarg factory function? @@ -141,8 +146,12 @@ policy - in real-world examples some combinations often should not run. -Use the `pytest_generate_tests`_ hook to solve both issues -and implement the `parametrization scheme of your choice`_. +However, with pytest-2.3 you can use the :ref:`@pytest.factory` decorator +and specify ``params`` so that all tests depending on the factory-created +resource will run multiple times with different parameters. + +You can also use the `pytest_generate_tests`_ hook to +implement the `parametrization scheme of your choice`_. .. _`pytest_generate_tests`: test/funcargs.html#parametrizing-tests .. _`parametrization scheme of your choice`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/ diff -r 90a3b3d49cebb9b6484251b7c7f6df1c317305d9 -r 4d9f48622c63733d4d67b436d40bdb14fb1ca037 doc/en/funcarg_compare.txt --- a/doc/en/funcarg_compare.txt +++ b/doc/en/funcarg_compare.txt @@ -88,7 +88,7 @@ sets. pytest-2.3 introduces a decorator for use on the factory itself:: @pytest.factory(params=["mysql", "pg"]) - def pytest_funcarg__db(testcontext): + def db(testcontext): ... # use testcontext.param Here the factory will be invoked twice (with the respective "mysql" @@ -105,7 +105,7 @@ Of course it's perfectly fine to combine parametrization and scoping:: @pytest.factory(scope="session", params=["mysql", "pg"]) - def pytest_funcarg__db(testcontext): + def db(testcontext): if testcontext.param == "mysql": db = MySQL() elif testcontext.param == "pg": @@ -137,7 +137,9 @@ def pytest_funcarg__db(request): ... -It is recommended to use the resource decorator, however. + +But it is then not possible to define scoping and parametrization. +It is thus recommended to use the factory decorator. solving per-session setup / the new @setup marker https://bitbucket.org/hpk42/pytest/changeset/57b9e4d9bdb2/ changeset: 57b9e4d9bdb2 user: hpk42 date: 2012-08-13 13:37:14 summary: fix issue172 so that @pytest.setup marked setup_module/function... functions are not called twice. Also fix ordering to that broader scoped setup functions are executed first. affected #: 3 files diff -r 4d9f48622c63733d4d67b436d40bdb14fb1ca037 -r 57b9e4d9bdb29039856691c16a2d7b5696d0eb89 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ Changes between 2.2.4 and 2.3.0.dev ----------------------------------- +- fix issue172 duplicate call of pytest.setup-decoratored setup_module + functions - fix junitxml=path construction so that if tests change the current working directory and the path is a relative path it is constructed correctly from the original current working dir. diff -r 4d9f48622c63733d4d67b436d40bdb14fb1ca037 -r 57b9e4d9bdb29039856691c16a2d7b5696d0eb89 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -363,24 +363,26 @@ return mod def setup(self): - if hasattr(self.obj, 'setup_module'): + setup_module = xunitsetup(self.obj, "setup_module") + if setup_module is not None: #XXX: nose compat hack, move to nose plugin # if it takes a positional arg, its probably a pytest style one # so we pass the current module object - if inspect.getargspec(self.obj.setup_module)[0]: - self.obj.setup_module(self.obj) + if inspect.getargspec(setup_module)[0]: + setup_module(self.obj) else: - self.obj.setup_module() + setup_module() def teardown(self): - if hasattr(self.obj, 'teardown_module'): + teardown_module = xunitsetup(self.obj, 'teardown_module') + if teardown_module is not None: #XXX: nose compat hack, move to nose plugin # if it takes a positional arg, its probably a py.test style one # so we pass the current module object - if inspect.getargspec(self.obj.teardown_module)[0]: - self.obj.teardown_module(self.obj) + if inspect.getargspec(teardown_module)[0]: + teardown_module(self.obj) else: - self.obj.teardown_module() + teardown_module() class Class(PyCollector): """ Collector for test methods. """ @@ -388,14 +390,14 @@ return [self._getcustomclass("Instance")(name="()", parent=self)] def setup(self): - setup_class = getattr(self.obj, 'setup_class', None) + setup_class = xunitsetup(self.obj, 'setup_class') if setup_class is not None: setup_class = getattr(setup_class, 'im_func', setup_class) setup_class = getattr(setup_class, '__func__', setup_class) setup_class(self.obj) def teardown(self): - teardown_class = getattr(self.obj, 'teardown_class', None) + teardown_class = xunitsetup(self.obj, 'teardown_class') if teardown_class is not None: teardown_class = getattr(teardown_class, 'im_func', teardown_class) teardown_class = getattr(teardown_class, '__func__', teardown_class) @@ -431,7 +433,7 @@ name = 'setup_method' else: name = 'setup_function' - setup_func_or_method = getattr(obj, name, None) + setup_func_or_method = xunitsetup(obj, name) if setup_func_or_method is not None: setup_func_or_method(self.obj) @@ -442,7 +444,7 @@ else: name = 'teardown_function' obj = self.parent.obj - teardown_func_or_meth = getattr(obj, name, None) + teardown_func_or_meth = xunitsetup(obj, name) if teardown_func_or_meth is not None: teardown_func_or_meth(self.obj) @@ -1338,6 +1340,7 @@ if nodeid.startswith(setupcall.baseid): l.append(setupcall) allargnames.update(setupcall.funcargnames) + l.sort(key=lambda x: x.scopenum) return l, allargnames @@ -1479,6 +1482,7 @@ self.func = func self.funcargnames = getfuncargnames(func) self.scope = scope + self.scopenum = scopes.index(scope) self.active = False self._finalizer = [] @@ -1595,3 +1599,9 @@ # argparams.append(key) return argparams + +def xunitsetup(obj, name): + meth = getattr(obj, name, None) + if meth is not None: + if not hasattr(meth, "_pytestsetup"): + return meth diff -r 4d9f48622c63733d4d67b436d40bdb14fb1ca037 -r 57b9e4d9bdb29039856691c16a2d7b5696d0eb89 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1874,6 +1874,28 @@ l = config._conftest.getconftestmodules(p)[0].l assert l == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2 + def test_setup_scope_ordering(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.setup(scope="function") + def fappend2(): + l.append(2) + @pytest.setup(scope="class") + def classappend3(): + l.append(3) + @pytest.setup(scope="module") + def mappend(): + l.append(1) + + class TestHallo: + def test_method(self): + assert l == [1,3,2] + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + + class TestFuncargMarker: def test_parametrize(self, testdir): testdir.makepyfile(""" @@ -2404,3 +2426,33 @@ """) reprec = testdir.inline_run() reprec.assertoutcome(passed=2) + +def test_setupdecorator_and_xunit(testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.setup(scope='module') + def setup_module(): + l.append("module") + @pytest.setup() + def setup_function(): + l.append("function") + + def test_func(): + pass + + class TestClass: + @pytest.setup(scope="class") + def setup_class(self): + l.append("class") + @pytest.setup() + def setup_method(self): + l.append("method") + def test_method(self): + pass + def test_all(): + assert l == ["module", "function", "class", + "function", "method", "function"] + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=3) Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Aug 16 13:23:44 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Thu, 16 Aug 2012 11:23:44 -0000 Subject: [py-svn] commit/pytest: 3 new changesets Message-ID: <20120816112344.8621.72221@bitbucket05.managed.contegix.com> 3 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/144204bdfb4e/ changeset: 144204bdfb4e user: Brianna Laugher date: 2012-08-10 07:44:58 summary: Fix URL for waskr project affected #: 2 files diff -r fa6a843aa98b1f8e9e95aa3b73c0bb4fcfc2e133 -r 144204bdfb4e4b35ecc68bf2fe5ec64dc9758a48 doc/en/projects.txt --- a/doc/en/projects.txt +++ b/doc/en/projects.txt @@ -23,7 +23,7 @@ * `bbfreeze `_ create standalone executables from Python scripts * `pdb++ `_ a fancier version of PDB * `py-s3fuse `_ Amazon S3 FUSE based filesystem -* `waskr `_ WSGI Stats Middleware +* `waskr `_ WSGI Stats Middleware * `guachi `_ global persistent configs for Python modules * `Circuits `_ lightweight Event Driven Framework * `pygtk-helpers `_ easy interaction with PyGTK diff -r fa6a843aa98b1f8e9e95aa3b73c0bb4fcfc2e133 -r 144204bdfb4e4b35ecc68bf2fe5ec64dc9758a48 doc/ja/projects.txt --- a/doc/ja/projects.txt +++ b/doc/ja/projects.txt @@ -31,7 +31,7 @@ * `bbfreeze `_ create standalone executables from Python scripts * `pdb++ `_ a fancier version of PDB * `py-s3fuse `_ Amazon S3 FUSE based filesystem - * `waskr `_ WSGI Stats Middleware + * `waskr `_ WSGI Stats Middleware * `guachi `_ global persistent configs for Python modules * `Circuits `_ lightweight Event Driven Framework * `pygtk-helpers `_ easy interaction with PyGTK https://bitbucket.org/hpk42/pytest/changeset/fd04a79b170f/ changeset: fd04a79b170f user: Brianna Laugher date: 2012-08-16 11:31:21 summary: expand list of projects based on URLs from holger affected #: 1 file diff -r 144204bdfb4e4b35ecc68bf2fe5ec64dc9758a48 -r fd04a79b170f333172c70a2952bf189dcbbb7f8a doc/en/projects.txt --- a/doc/en/projects.txt +++ b/doc/en/projects.txt @@ -38,6 +38,17 @@ * `bu `_ a microscopic build system * `katcp `_ Telescope communication protocol over Twisted * `kss plugin timer `_ +* `pyudev `_ a pure Python binding to the Linux library libudev +* `pytest-localserver `_ a plugin for pytest that provides a httpserver and smtpserver +* `pytest-monkeyplus `_ a plugin that extends monkeypatch + +These projects help integrate py.test into other Python frameworks: + +* `pytest-django `_ for Django +* `zope.pytest `_ for Zope and Grok +* `pytest__gae `_ for Google App Engine +* There is `some work `_ underway for Kotti, a CMS built in Pyramid/Pylons + Some organisations using py.test ----------------------------------- https://bitbucket.org/hpk42/pytest/changeset/5c1e6d977aec/ changeset: 5c1e6d977aec user: hpk42 date: 2012-08-16 13:23:42 summary: Merged in pfctdayelise/pytest (pull request #17) affected #: 2 files diff -r 57b9e4d9bdb29039856691c16a2d7b5696d0eb89 -r 5c1e6d977aec371021a7ed527f4e362b8739d840 doc/en/projects.txt --- a/doc/en/projects.txt +++ b/doc/en/projects.txt @@ -23,7 +23,7 @@ * `bbfreeze `_ create standalone executables from Python scripts * `pdb++ `_ a fancier version of PDB * `py-s3fuse `_ Amazon S3 FUSE based filesystem -* `waskr `_ WSGI Stats Middleware +* `waskr `_ WSGI Stats Middleware * `guachi `_ global persistent configs for Python modules * `Circuits `_ lightweight Event Driven Framework * `pygtk-helpers `_ easy interaction with PyGTK @@ -38,6 +38,17 @@ * `bu `_ a microscopic build system * `katcp `_ Telescope communication protocol over Twisted * `kss plugin timer `_ +* `pyudev `_ a pure Python binding to the Linux library libudev +* `pytest-localserver `_ a plugin for pytest that provides a httpserver and smtpserver +* `pytest-monkeyplus `_ a plugin that extends monkeypatch + +These projects help integrate py.test into other Python frameworks: + +* `pytest-django `_ for Django +* `zope.pytest `_ for Zope and Grok +* `pytest__gae `_ for Google App Engine +* There is `some work `_ underway for Kotti, a CMS built in Pyramid/Pylons + Some organisations using py.test ----------------------------------- diff -r 57b9e4d9bdb29039856691c16a2d7b5696d0eb89 -r 5c1e6d977aec371021a7ed527f4e362b8739d840 doc/ja/projects.txt --- a/doc/ja/projects.txt +++ b/doc/ja/projects.txt @@ -31,7 +31,7 @@ * `bbfreeze `_ create standalone executables from Python scripts * `pdb++ `_ a fancier version of PDB * `py-s3fuse `_ Amazon S3 FUSE based filesystem - * `waskr `_ WSGI Stats Middleware + * `waskr `_ WSGI Stats Middleware * `guachi `_ global persistent configs for Python modules * `Circuits `_ lightweight Event Driven Framework * `pygtk-helpers `_ easy interaction with PyGTK Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Fri Aug 17 16:08:47 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Fri, 17 Aug 2012 14:08:47 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: fix issue #178 and extend the failure escape test Message-ID: <20120817140847.10966.50613@bitbucket16.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/0f4d63deaf66/ changeset: 0f4d63deaf66 user: RonnyPfannschmidt date: 2012-08-17 16:08:08 summary: fix issue #178 and extend the failure escape test affected #: 3 files diff -r 5c1e6d977aec371021a7ed527f4e362b8739d840 -r 0f4d63deaf661256b159f8f089999034c30e1d92 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -42,6 +42,8 @@ especially with respect to the "magic" history, also mention pytest-django, trial and unittest integration. +- fix issue 178: xml binary escapes are now wrapped in py.xml.raw + - reporting refinements: - pytest_report_header now receives a "startdir" so that diff -r 5c1e6d977aec371021a7ed527f4e362b8739d840 -r 0f4d63deaf661256b159f8f089999034c30e1d92 _pytest/junitxml.py --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -57,7 +57,7 @@ return unicode('#x%02X') % i else: return unicode('#x%04X') % i - return illegal_xml_re.sub(repl, py.xml.escape(arg)) + return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg))) def pytest_addoption(parser): group = parser.getgroup("terminal reporting") diff -r 5c1e6d977aec371021a7ed527f4e362b8739d840 -r 0f4d63deaf661256b159f8f089999034c30e1d92 testing/test_junitxml.py --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -159,24 +159,27 @@ def test_failure_escape(self, testdir): testdir.makepyfile(""" - def pytest_generate_tests(metafunc): - metafunc.addcall(id="<", funcargs=dict(arg1=42)) - metafunc.addcall(id="&", funcargs=dict(arg1=44)) + import pytest + @pytest.mark.parametrize('arg1', "<&'", ids="<&'") def test_func(arg1): + print arg1 assert 0 """) result, dom = runandparse(testdir) assert result.ret node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, failures=2, tests=2) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - classname="test_failure_escape", - name="test_func[<]") - tnode = node.getElementsByTagName("testcase")[1] - assert_attr(tnode, - classname="test_failure_escape", - name="test_func[&]") + assert_attr(node, failures=3, tests=3) + + for index, char in enumerate("<&'"): + + tnode = node.getElementsByTagName("testcase")[index] + assert_attr(tnode, + classname="test_failure_escape", + name="test_func[%s]" % char) + sysout = tnode.getElementsByTagName('system-out')[0] + text = sysout.childNodes[0].wholeText + assert text == '%s\n' % char + def test_junit_prefixing(self, testdir): testdir.makepyfile(""" Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sun Aug 19 12:37:06 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sun, 19 Aug 2012 10:37:06 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: ignore magic callables with no sane code in factory/setup discovery Message-ID: <20120819103706.17111.40944@bitbucket15.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/4797ffd81621/ changeset: 4797ffd81621 user: RonnyPfannschmidt date: 2012-08-19 12:36:49 summary: ignore magic callables with no sane code in factory/setup discovery affected #: 3 files diff -r 0f4d63deaf661256b159f8f089999034c30e1d92 -r 4797ffd8162188aa2a82fd725bcc0483bdd008fd CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,9 @@ - fix issue 178: xml binary escapes are now wrapped in py.xml.raw +- factory discovery no longer fails with magic global callables + that provide no sane __code__ object (mock.call for example) + - reporting refinements: - pytest_report_header now receives a "startdir" so that diff -r 0f4d63deaf661256b159f8f089999034c30e1d92 -r 4797ffd8162188aa2a82fd725bcc0483bdd008fd _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1303,10 +1303,14 @@ obj = getattr(holderobj, name) if not callable(obj): continue + # to avoid breaking on magic global callables + # we explicitly check if we get a sane code object + # else having mock.call in the globals fails for example + code = py.code.getrawcode(obj) + if not inspect.iscode(code): + continue # resource factories either have a pytest_funcarg__ prefix # or are "funcarg" marked - if not callable(obj): - continue marker = getattr(obj, "_pytestfactory", None) if marker is not None: assert not name.startswith(self._argprefix) diff -r 0f4d63deaf661256b159f8f089999034c30e1d92 -r 4797ffd8162188aa2a82fd725bcc0483bdd008fd testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1761,6 +1761,23 @@ reprec = testdir.inline_run("-s") reprec.assertoutcome(passed=1) + def test_callables_nocode(self, testdir): + """ + a imported mock.call would break setup/factory discovery + due to it being callable and __code__ not being a code object + """ + testdir.makepyfile(""" + class _call(tuple): + def __call__(self, *k, **kw): + pass + def __getattr__(self, k): + return self + + call = _call() + """) + reprec = testdir.inline_run("-s") + reprec.assertoutcome(failed=0, passed=0) + class TestSetupManagement: def test_funcarg_and_setup(self, testdir): Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sun Aug 19 13:46:01 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sun, 19 Aug 2012 11:46:01 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: fix issue 176: raises(AssertionError) now catches builtin AssertionError as well Message-ID: <20120819114601.4317.6992@bitbucket02.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/60f144dcdcf4/ changeset: 60f144dcdcf4 user: RonnyPfannschmidt date: 2012-08-19 13:45:26 summary: fix issue 176: raises(AssertionError) now catches builtin AssertionError as well affected #: 3 files diff -r 4797ffd8162188aa2a82fd725bcc0483bdd008fd -r 60f144dcdcf4a488ef1114ead43a7d59df8f4663 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -44,6 +44,10 @@ - fix issue 178: xml binary escapes are now wrapped in py.xml.raw +- fix issue 176: correctly catch the builtin AssertionError + even when we replaced AssertionError with a subclass on the + python level + - factory discovery no longer fails with magic global callables that provide no sane __code__ object (mock.call for example) diff -r 4797ffd8162188aa2a82fd725bcc0483bdd008fd -r 60f144dcdcf4a488ef1114ead43a7d59df8f4663 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -777,6 +777,11 @@ """ __tracebackhide__ = True + if ExpectedException is AssertionError: + # we want to catch a AssertionError + # replace our subclass with the builtin one + # see https://bitbucket.org/hpk42/pytest/issue/176/pytestraises + from exceptions import AssertionError as ExpectedException if not args: return RaisesContext(ExpectedException) diff -r 4797ffd8162188aa2a82fd725bcc0483bdd008fd -r 60f144dcdcf4a488ef1114ead43a7d59df8f4663 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1419,6 +1419,14 @@ except pytest.raises.Exception: pass + def test_raises_flip_builtin_AssertionError(self): + # we replace AssertionError on python level + # however c code might still raise the builtin one + import exceptions + pytest.raises(AssertionError,""" + raise exceptions.AssertionError + """) + @pytest.mark.skipif('sys.version < "2.5"') def test_raises_as_contextmanager(self, testdir): testdir.makepyfile(""" Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Sun Aug 19 14:57:33 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sun, 19 Aug 2012 12:57:33 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: exchange the rawcode factory marker check with a more robust and specific instance check as advised by holger Message-ID: <20120819125733.28677.32609@bitbucket24.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/bb0752e6ea8a/ changeset: bb0752e6ea8a user: RonnyPfannschmidt date: 2012-08-19 14:57:07 summary: exchange the rawcode factory marker check with a more robust and specific instance check as advised by holger affected #: 1 file diff -r 60f144dcdcf4a488ef1114ead43a7d59df8f4663 -r bb0752e6ea8a545f84c955eeb0ec8e5bb0f26475 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1308,16 +1308,14 @@ obj = getattr(holderobj, name) if not callable(obj): continue - # to avoid breaking on magic global callables - # we explicitly check if we get a sane code object - # else having mock.call in the globals fails for example - code = py.code.getrawcode(obj) - if not inspect.iscode(code): - continue # resource factories either have a pytest_funcarg__ prefix # or are "funcarg" marked marker = getattr(obj, "_pytestfactory", None) if marker is not None: + if not isinstance(marker, FactoryMarker): + # magic globals with __getattr__ + # give us something thats wrong for that case + continue assert not name.startswith(self._argprefix) argname = name scope = marker.scope Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Aug 22 19:50:18 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 22 Aug 2012 17:50:18 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: add xfailing test for issue 179 Message-ID: <20120822175018.26029.71273@bitbucket05.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/c0af4c88b90e/ changeset: c0af4c88b90e user: RonnyPfannschmidt date: 2012-08-22 19:49:50 summary: add xfailing test for issue 179 affected #: 1 file diff -r bb0752e6ea8a545f84c955eeb0ec8e5bb0f26475 -r c0af4c88b90e5230386f0378da674184ef980c38 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1664,6 +1664,24 @@ "*2 passed*" ]) + @pytest.mark.xfail(reason="factorydef passed to tw.line") + def test_factory_uses_unknown_funcarg_error(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.factory(scope='session') + def arg1(missing): + return + + def test_missing(arg1): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*LookupError: no factory found for argument 'missing'" + ]) + + class TestResourceIntegrationFunctional: def test_parametrize_with_ids(self, testdir): Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Wed Aug 22 21:44:02 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 22 Aug 2012 19:44:02 -0000 Subject: [py-svn] commit/pytest: 2 new changesets Message-ID: <20120822194402.22824.44841@bitbucket25.managed.contegix.com> 2 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/1d2727fd33f9/ changeset: 1d2727fd33f9 user: RonnyPfannschmidt date: 2012-08-22 21:20:18 summary: correctly have the test for issue #[C179 actually fail affected #: 1 file diff -r c0af4c88b90e5230386f0378da674184ef980c38 -r 1d2727fd33f984f7c4860e803c39809721b074e6 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1668,12 +1668,16 @@ def test_factory_uses_unknown_funcarg_error(self, testdir): testdir.makepyfile(""" import pytest - - @pytest.factory(scope='session') - def arg1(missing): + + @pytest.factory() + def fail(missing): return - def test_missing(arg1): + @pytest.factory() + def call_fail(fail): + return + + def test_missing(call_fail): pass """) result = testdir.runpytest() https://bitbucket.org/hpk42/pytest/changeset/7aab1fcc72cd/ changeset: 7aab1fcc72cd user: RonnyPfannschmidt date: 2012-08-22 21:43:42 summary: fix issue 179 - propperly show the dependency chain of factories on setup failure affected #: 3 files diff -r 1d2727fd33f984f7c4860e803c39809721b074e6 -r 7aab1fcc72cd250cb161231bce28bf57559d5f01 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -35,6 +35,8 @@ - fix issue128: show captured output when capsys/capfd are used +- fix issue179: propperly show the dependency chain of factories + - pluginmanager.register(...) now raises ValueError if the plugin has been already registered or the name is taken diff -r 1d2727fd33f984f7c4860e803c39809721b074e6 -r 7aab1fcc72cd250cb161231bce28bf57559d5f01 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1211,8 +1211,14 @@ def toterminal(self, tw): tw.line() - for line in self.factblines or []: - tw.line(line) + if self.factblines: + tw.line(' dependency of:') + for factorydef in self.factblines: + tw.line(' %s in %s' % ( + factorydef.argname, + factorydef.baseid, + )) + tw.line() for line in self.deflines: tw.line(" " + line.strip()) for line in self.errorstring.split("\n"): diff -r 1d2727fd33f984f7c4860e803c39809721b074e6 -r 7aab1fcc72cd250cb161231bce28bf57559d5f01 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1664,8 +1664,7 @@ "*2 passed*" ]) - @pytest.mark.xfail(reason="factorydef passed to tw.line") - def test_factory_uses_unknown_funcarg_error(self, testdir): + def test_factory_uses_unknown_funcarg_as_dependency_error(self, testdir): testdir.makepyfile(""" import pytest @@ -1682,7 +1681,10 @@ """) result = testdir.runpytest() result.stdout.fnmatch_lines([ - "*LookupError: no factory found for argument 'missing'" + "*dependency of:*", + "*call_fail*", + "*def fail(*", + "*LookupError: no factory found for argument 'missing'", ]) Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Aug 28 23:19:48 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Tue, 28 Aug 2012 21:19:48 -0000 Subject: [py-svn] commit/pytest: 2 new changesets Message-ID: <20120828211948.5141.24438@bitbucket24.managed.contegix.com> 2 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/08e928984d17/ changeset: 08e928984d17 user: gutworth date: 2012-08-28 22:35:06 summary: remove usage of exception module, which is gone in py3.3 affected #: 2 files diff -r 7aab1fcc72cd250cb161231bce28bf57559d5f01 -r 08e928984d178f92a5b75dcc4c0fd60dd3c1b6b7 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -781,7 +781,7 @@ # we want to catch a AssertionError # replace our subclass with the builtin one # see https://bitbucket.org/hpk42/pytest/issue/176/pytestraises - from exceptions import AssertionError as ExpectedException + from _pytest.assertion.util import BuiltinAssertionError as ExpectedException if not args: return RaisesContext(ExpectedException) diff -r 7aab1fcc72cd250cb161231bce28bf57559d5f01 -r 08e928984d178f92a5b75dcc4c0fd60dd3c1b6b7 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1422,9 +1422,9 @@ def test_raises_flip_builtin_AssertionError(self): # we replace AssertionError on python level # however c code might still raise the builtin one - import exceptions + from _pytest.assertion.util import BuiltinAssertionError pytest.raises(AssertionError,""" - raise exceptions.AssertionError + raise BuiltinAssertionError """) @pytest.mark.skipif('sys.version < "2.5"') https://bitbucket.org/hpk42/pytest/changeset/a0ec8dac668c/ changeset: a0ec8dac668c user: gutworth date: 2012-08-28 22:37:43 summary: use py3 compatible print syntax affected #: 1 file diff -r 08e928984d178f92a5b75dcc4c0fd60dd3c1b6b7 -r a0ec8dac668c75ae89a5708d75943daecd91c238 testing/test_junitxml.py --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -162,7 +162,7 @@ import pytest @pytest.mark.parametrize('arg1', "<&'", ids="<&'") def test_func(arg1): - print arg1 + print(arg1) assert 0 """) result, dom = runandparse(testdir) Repository URL: https://bitbucket.org/hpk42/pytest/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.