diff --git a/doc/test/funcargs.txt b/doc/test/funcargs.txt index 0b1631612..406ac9fce 100644 --- a/doc/test/funcargs.txt +++ b/doc/test/funcargs.txt @@ -120,21 +120,54 @@ to access test configuration and test context: ``request.param``: if exists was passed by a `parametrizing test generator`_ +perform scoped setup and teardown +--------------------------------------------- + +.. sourcecode:: python + + def cached_setup(setup, teardown=None, scope="module", keyextra=None): + """ setup and return value of calling setup(), cache results and + optionally teardown the value by calling ``teardown(value)``. The scope + determines the key for cashing the setup value. Specify ``keyextra`` + to add to the cash-key. + scope == 'function': when test function run finishes. + scope == 'module': when tests in a different module are run + scope == 'run': when the test run has been finished. + """ + +example for providing a value that is to be setup only once during a test run: + +.. sourcecode:: python + + def pytest_funcarg__db(request): + return request.cached_setup( + lambda: ExpensiveSetup(request.config.option.db), + lambda val: val.close(), + scope="run" + ) + cleanup after test function execution --------------------------------------------- -Request objects allow to **register a finalizer method** which is -called after a test function has finished running. -This is useful for tearing down or cleaning up -test state related to a function argument. Here is a basic -example for providing a ``myfile`` object that will be -closed upon test function finish: +.. sourcecode:: python + + def addfinalizer(func, scope="function"): + """ register calling a a finalizer function. + scope == 'function': when test function run finishes. + scope == 'module': when tests in a different module are run + scope == 'run': when all tests have been run. + """ + +Calling ``request.addfinalizer()`` is useful for scheduling teardown +functions. The given scope determines when the teardown function +will be called. Here is a basic example for providing a ``myfile`` +object that is to be closed when the test function finishes. .. sourcecode:: python def pytest_funcarg__myfile(self, request): - # ... create and open a "myfile" object ... + # ... create and open a unique per-function "myfile" object ... request.addfinalizer(lambda: myfile.close()) return myfile diff --git a/py/test/funcargs.py b/py/test/funcargs.py index a4a1ca61b..656ccd6ba 100644 --- a/py/test/funcargs.py +++ b/py/test/funcargs.py @@ -89,14 +89,36 @@ class FuncargRequest: attrname=self._argprefix + str(argname) ) + def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): + if not hasattr(self.config, '_setupcache'): + self.config._setupcache = {} + cachekey = (self._getscopeitem(scope), extrakey) + cache = self.config._setupcache + try: + val = cache[cachekey] + except KeyError: + val = setup() + cache[cachekey] = val + if teardown is not None: + self.addfinalizer(lambda: teardown(val), scope=scope) + return val + def call_next_provider(self): if not self._provider: raise self.Error("no provider methods left") next_provider = self._provider.pop() return next_provider(request=self) - def addfinalizer(self, finalizer): - self._pyfuncitem.addfinalizer(finalizer) + def _getscopeitem(self, scope): + if scope == "function": + return self._pyfuncitem + elif scope == "module": + return self._pyfuncitem._getparent(py.test.collect.Module) + raise ValueError("unknown finalization scope %r" %(scope,)) + + def addfinalizer(self, finalizer, scope="function"): + colitem = self._getscopeitem(scope) + self.config._setupstate.addfinalizer(finalizer=finalizer, colitem=colitem) def __repr__(self): return "" %(self.argname, self._pyfuncitem) diff --git a/py/test/pycollect.py b/py/test/pycollect.py index 957f15318..3a8774a09 100644 --- a/py/test/pycollect.py +++ b/py/test/pycollect.py @@ -328,7 +328,6 @@ class Function(FunctionMixin, py.test.collect.Item): def __init__(self, name, parent=None, config=None, args=(), callspec=None, callobj=_dummy): super(Function, self).__init__(name, parent, config=config) - self._finalizers = [] self._args = args if args: assert not callspec, "yielded functions (deprecated) cannot have funcargs" @@ -339,16 +338,9 @@ class Function(FunctionMixin, py.test.collect.Item): if callobj is not _dummy: self._obj = callobj - def addfinalizer(self, func): - self._finalizers.append(func) + #def addfinalizer(self, func): + # self.config._setupstate.ddfinalizer(func, colitem=self) - def teardown(self): - finalizers = self._finalizers - while finalizers: - call = finalizers.pop() - call() - super(Function, self).teardown() - def readkeywords(self): d = super(Function, self).readkeywords() d.update(self.obj.func_dict) @@ -370,7 +362,10 @@ class Function(FunctionMixin, py.test.collect.Item): return (self.name == other.name and self._args == other._args and self.parent == other.parent and - self.obj == other.obj) + self.obj == other.obj and + getattr(self, '_requestparam', None) == + getattr(other, '_requestparam', None) + ) except AttributeError: pass return False diff --git a/py/test/runner.py b/py/test/runner.py index 9c3c5fa83..8d066aaf1 100644 --- a/py/test/runner.py +++ b/py/test/runner.py @@ -172,16 +172,32 @@ class SetupState(object): """ shared state for setting up/tearing down test items or collectors. """ def __init__(self): self.stack = [] + self._finalizers = {} + + def addfinalizer(self, finalizer, colitem): + assert callable(finalizer) + #assert colitem in self.stack + self._finalizers.setdefault(colitem, []).append(finalizer) + + def _teardown(self, colitem): + finalizers = self._finalizers.pop(colitem, None) + while finalizers: + fin = finalizers.pop() + fin() + colitem.teardown() + for colitem in self._finalizers: + assert colitem in self.stack def teardown_all(self): while self.stack: col = self.stack.pop() - col.teardown() + self._teardown(col) + assert not self._finalizers def teardown_exact(self, item): if self.stack and self.stack[-1] == item: col = self.stack.pop() - col.teardown() + self._teardown(col) def prepare(self, colitem): """ setup objects along the collector chain to the test-method @@ -191,7 +207,7 @@ class SetupState(object): if self.stack == needed_collectors[:len(self.stack)]: break col = self.stack.pop() - col.teardown() + self._teardown(col) for col in needed_collectors[len(self.stack):]: col.setup() self.stack.append(col) diff --git a/py/test/testing/test_conftesthandle.py b/py/test/testing/test_conftesthandle.py index e199b8b0c..9af9aeeeb 100644 --- a/py/test/testing/test_conftesthandle.py +++ b/py/test/testing/test_conftesthandle.py @@ -1,7 +1,7 @@ import py from py.__.test.conftesthandle import Conftest -def pytest_generate_tests(metafunc, generator): +def pytest_generate_tests(metafunc): if "basedir" in metafunc.funcargnames: metafunc.addcall(param="global") metafunc.addcall(param="inpackage") @@ -15,8 +15,7 @@ def pytest_funcarg__basedir(request): d.ensure("adir/__init__.py") d.ensure("adir/b/__init__.py") return d - return request.cached_setup(perclass=basedirmaker) - return request.cached_setup(perclass=basedirmaker) + return request.cached_setup(lambda: basedirmaker(request), extrakey=request.param) class TestConftestValueAccessGlobal: def test_basic_init(self, basedir): diff --git a/py/test/testing/test_funcargs.py b/py/test/testing/test_funcargs.py index d9a263ea6..6aaed0aa0 100644 --- a/py/test/testing/test_funcargs.py +++ b/py/test/testing/test_funcargs.py @@ -121,9 +121,73 @@ class TestRequest: def test_func(something): pass """) req = funcargs.FuncargRequest(item, "something") + py.test.raises(ValueError, "req.addfinalizer(None, scope='xyz')") l = [1] req.addfinalizer(l.pop) - item.teardown() + req.config._setupstate._teardown(item) + assert not l + + def test_request_cachedsetup(self, testdir): + item1,item2 = testdir.getitems(""" + class TestClass: + def test_func1(self, something): + pass + def test_func2(self, something): + pass + """) + req1 = funcargs.FuncargRequest(item1, "something") + l = ["hello"] + def setup(): + return l.pop() + ret1 = req1.cached_setup(setup) + assert ret1 == "hello" + ret1b = req1.cached_setup(setup) + assert ret1 == ret1b + req2 = funcargs.FuncargRequest(item2, "something") + ret2 = req2.cached_setup(setup) + assert ret2 == ret1 + + def test_request_cachedsetup_extrakey(self, testdir): + item1 = testdir.getitem("def test_func(): pass") + req1 = funcargs.FuncargRequest(item1, "something") + l = ["hello", "world"] + def setup(): + return l.pop() + ret1 = req1.cached_setup(setup, extrakey=1) + ret2 = req1.cached_setup(setup, extrakey=2) + assert ret2 == "hello" + assert ret1 == "world" + ret1b = req1.cached_setup(setup, extrakey=1) + ret2b = req1.cached_setup(setup, extrakey=2) + assert ret1 == ret1b + assert ret2 == ret2b + + def test_request_cached_setup_functional(self, testdir): + testdir.makepyfile(test_0=""" + l = [] + def pytest_funcarg__something(request): + val = request.cached_setup(setup, teardown) + return val + def setup(mycache=[1]): + l.append(mycache.pop()) + return l + def teardown(something): + l.remove(something[0]) + l.append(2) + def test_list_once(something): + assert something == [1] + def test_list_twice(something): + assert something == [1] + """) + testdir.makepyfile(test_1=""" + import test_0 # should have run already + def test_check_test0_has_teardown_correct(): + assert test_0.l == [2] + """) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines([ + "*3 passed*" + ]) def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") diff --git a/py/test/testing/test_pycollect.py b/py/test/testing/test_pycollect.py index 807a750c7..3a8e11d72 100644 --- a/py/test/testing/test_pycollect.py +++ b/py/test/testing/test_pycollect.py @@ -242,6 +242,19 @@ class TestFunction: assert f1 == f1_b assert not f1 != f1_b + class callspec1: + param = 1 + funcargs = {} + class callspec2: + param = 2 + funcargs = {} + f5 = py.test.collect.Function(name="name", config=config, + callspec=callspec1, callobj=isinstance) + f5b = py.test.collect.Function(name="name", config=config, + callspec=callspec2, callobj=isinstance) + assert f5 != f5b + assert not (f5 == f5b) + class TestSorting: def test_check_equality_and_cmp_basic(self, testdir): modcol = testdir.getmodulecol(""" diff --git a/py/test/testing/test_runner.py b/py/test/testing/test_runner.py index cca4b6045..7005583db 100644 --- a/py/test/testing/test_runner.py +++ b/py/test/testing/test_runner.py @@ -2,6 +2,15 @@ from py.__.test.config import SetupState class TestSetupState: + def test_setup(self, testdir): + ss = SetupState() + item = testdir.getitem("def test_func(): pass") + l = [1] + ss.addfinalizer(l.pop, colitem=item) + ss._teardown(item) + assert not l + +class TestSetupStateFunctional: disabled = True def test_setup_ok(self, testdir): item = testdir.getitem(""" @@ -90,3 +99,4 @@ class TestSetupState: assert event.excinfo +