ref #322 cleanup all teardown calling to only happen when setup succeeded.

don't use autouse fixtures for now because it would cause a proliferation
and overhead for the execution of every test.  Rather introduce a
node.addfinalizer(fin) to attach a finalizer to the respective node
and call it from node.setup() functions if the setup phase succeeded
(i.e. there is no setup function or it finished successfully)
This commit is contained in:
holger krekel 2013-08-02 09:52:40 +02:00
parent b2ebb80878
commit 7d86827b5e
10 changed files with 151 additions and 89 deletions

View File

@ -2,9 +2,10 @@ Changes between 2.3.5 and 2.4.DEV
----------------------------------- -----------------------------------
- fix issue322: tearDownClass is not run if setUpClass failed. Thanks - fix issue322: tearDownClass is not run if setUpClass failed. Thanks
Mathieu Agopian for fixing. The patch moves handling setUpClass Mathieu Agopian for the initial fix. Also make all of pytest/nose finalizer
into a new autofixture. (XXX impl-decide if rather adding addfinalizer() mimick the same generic behaviour: if a setupX exists and fails,
API to node's would have a similar effect) don't run teardownX. This also introduces a new method "node.addfinalizer()"
helper which can only be called during the setup phase of a node.
- fix issue336: autouse fixture in plugins should work again. - fix issue336: autouse fixture in plugins should work again.

View File

@ -1,2 +1,2 @@
# #
__version__ = '2.4.0.dev8' __version__ = '2.4.0.dev10'

View File

@ -41,7 +41,7 @@ def pytest_addoption(parser):
help="run pytest in strict mode, warnings become errors.") help="run pytest in strict mode, warnings become errors.")
group = parser.getgroup("collect", "collection") group = parser.getgroup("collect", "collection")
group.addoption('--collectonly', '--collect-only', action="store_true", group.addoption('--collectonly', '--collect-only', action="store_true",
help="only collect tests, don't execute them."), help="only collect tests, don't execute them."),
group.addoption('--pyargs', action="store_true", group.addoption('--pyargs', action="store_true",
help="try to interpret all arguments as python packages.") help="try to interpret all arguments as python packages.")
@ -326,6 +326,14 @@ class Node(object):
def getplugins(self): def getplugins(self):
return self.config._getmatchingplugins(self.fspath) return self.config._getmatchingplugins(self.fspath)
def addfinalizer(self, fin):
""" register a function to be called when this node is finalized.
This method can only be called when this node is active
in a setup chain, for example during self.setup().
"""
self.session._setupstate.addfinalizer(fin, self)
def getparent(self, cls): def getparent(self, cls):
current = self current = self
while current and not isinstance(current, cls): while current and not isinstance(current, cls):

View File

@ -384,19 +384,19 @@ class Module(pytest.File, PyCollector):
setup_module(self.obj) setup_module(self.obj)
else: else:
setup_module() setup_module()
fin = getattr(self.obj, 'tearDownModule', None)
def teardown(self): if fin is None:
teardown_module = xunitsetup(self.obj, 'tearDownModule') fin = getattr(self.obj, 'teardown_module', None)
if teardown_module is None: if fin is not None:
teardown_module = xunitsetup(self.obj, 'teardown_module')
if teardown_module is not None:
#XXX: nose compat hack, move to nose plugin #XXX: nose compat hack, move to nose plugin
# if it takes a positional arg, its probably a py.test style one # if it takes a positional arg, its probably a py.test style one
# so we pass the current module object # so we pass the current module object
if inspect.getargspec(teardown_module)[0]: if inspect.getargspec(fin)[0]:
teardown_module(self.obj) finalizer = lambda: fin(self.obj)
else: else:
teardown_module() finalizer = fin
self.addfinalizer(finalizer)
class Class(PyCollector): class Class(PyCollector):
""" Collector for test methods. """ """ Collector for test methods. """
@ -415,12 +415,11 @@ class Class(PyCollector):
setup_class = getattr(setup_class, '__func__', setup_class) setup_class = getattr(setup_class, '__func__', setup_class)
setup_class(self.obj) setup_class(self.obj)
def teardown(self): fin_class = getattr(self.obj, 'teardown_class', None)
teardown_class = xunitsetup(self.obj, 'teardown_class') if fin_class is not None:
if teardown_class is not None: fin_class = getattr(fin_class, 'im_func', fin_class)
teardown_class = getattr(teardown_class, 'im_func', teardown_class) fin_class = getattr(fin_class, '__func__', fin_class)
teardown_class = getattr(teardown_class, '__func__', teardown_class) self.addfinalizer(lambda: fin_class(self.obj))
teardown_class(self.obj)
class Instance(PyCollector): class Instance(PyCollector):
def _getobj(self): def _getobj(self):
@ -449,23 +448,17 @@ class FunctionMixin(PyobjMixin):
else: else:
obj = self.parent.obj obj = self.parent.obj
if inspect.ismethod(self.obj): if inspect.ismethod(self.obj):
name = 'setup_method' setup_name = 'setup_method'
teardown_name = 'teardown_method'
else: else:
name = 'setup_function' setup_name = 'setup_function'
setup_func_or_method = xunitsetup(obj, name) teardown_name = 'teardown_function'
setup_func_or_method = xunitsetup(obj, setup_name)
if setup_func_or_method is not None: if setup_func_or_method is not None:
setup_func_or_method(self.obj) setup_func_or_method(self.obj)
fin = getattr(obj, teardown_name, None)
def teardown(self): if fin is not None:
""" perform teardown for this test function. """ self.addfinalizer(lambda: fin(self.obj))
if inspect.ismethod(self.obj):
name = 'teardown_method'
else:
name = 'teardown_function'
obj = self.parent.obj
teardown_func_or_meth = xunitsetup(obj, name)
if teardown_func_or_meth is not None:
teardown_func_or_meth(self.obj)
def _prunetraceback(self, excinfo): def _prunetraceback(self, excinfo):
if hasattr(self, '_obj') and not self.config.option.fulltrace: if hasattr(self, '_obj') and not self.config.option.fulltrace:

View File

@ -19,21 +19,6 @@ def is_unittest(obj):
return False return False
@pytest.fixture(scope='class', autouse=True)
def _xunit_setUpClass(request):
"""Add support for unittest.TestCase setUpClass and tearDownClass."""
if not is_unittest(request.cls):
return # only support setUpClass / tearDownClass for unittest.TestCase
if getattr(request.cls, '__unittest_skip__', False):
return # skipped
setup = getattr(request.cls, 'setUpClass', None)
teardown = getattr(request.cls, 'tearDownClass', None)
if setup is not None:
setup()
if teardown is not None:
request.addfinalizer(teardown)
def pytest_pycollect_makeitem(collector, name, obj): def pytest_pycollect_makeitem(collector, name, obj):
if is_unittest(obj): if is_unittest(obj):
return UnitTestCase(name, parent=collector) return UnitTestCase(name, parent=collector)
@ -42,6 +27,18 @@ def pytest_pycollect_makeitem(collector, name, obj):
class UnitTestCase(pytest.Class): class UnitTestCase(pytest.Class):
nofuncargs = True # marker for fixturemanger.getfixtureinfo() nofuncargs = True # marker for fixturemanger.getfixtureinfo()
# to declare that our children do not support funcargs # to declare that our children do not support funcargs
#
def setup(self):
cls = self.obj
if getattr(cls, '__unittest_skip__', False):
return # skipped
setup = getattr(cls, 'setUpClass', None)
if setup is not None:
setup()
teardown = getattr(cls, 'tearDownClass', None)
if teardown is not None:
self.addfinalizer(teardown)
super(UnitTestCase, self).setup()
def collect(self): def collect(self):
self.session._fixturemanager.parsefactories(self, unittest=True) self.session._fixturemanager.parsefactories(self, unittest=True)

View File

@ -13,8 +13,15 @@ names). While these setup/teardown methods are and will remain fully
supported you may also use pytest's more powerful :ref:`fixture mechanism supported you may also use pytest's more powerful :ref:`fixture mechanism
<fixture>` which leverages the concept of dependency injection, allowing <fixture>` which leverages the concept of dependency injection, allowing
for a more modular and more scalable approach for managing test state, for a more modular and more scalable approach for managing test state,
especially for larger projects and for functional testing. It is safe especially for larger projects and for functional testing. You can
to mix both fixture mechanisms. mix both fixture mechanisms in the same file but unittest-based
test methods cannot receive fixture arguments.
.. note::
As of pytest-2.4, teardownX functions are not called if
setupX existed and failed/was skipped. This harmonizes
behaviour across all major python testing tools.
Module level setup/teardown Module level setup/teardown
-------------------------------------- --------------------------------------

View File

@ -11,7 +11,7 @@ def main():
name='pytest', name='pytest',
description='py.test: simple powerful testing with Python', description='py.test: simple powerful testing with Python',
long_description = long_description, long_description = long_description,
version='2.4.0.dev8', version='2.4.0.dev10',
url='http://pytest.org', url='http://pytest.org',
license='MIT license', license='MIT license',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],

View File

@ -834,8 +834,6 @@ class TestFixtureUsages:
l = reprec.getfailedcollections() l = reprec.getfailedcollections()
assert len(l) == 1 assert len(l) == 1
@pytest.mark.xfail(reason="unclear if it should be supported at all, "
"currently broken")
def test_request_can_be_overridden(self, testdir): def test_request_can_be_overridden(self, testdir):
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest

View File

@ -32,6 +32,39 @@ def test_module_and_function_setup(testdir):
rep = reprec.matchreport("test_module") rep = reprec.matchreport("test_module")
assert rep.passed assert rep.passed
def test_module_setup_failure_no_teardown(testdir):
reprec = testdir.inline_runsource("""
l = []
def setup_module(module):
l.append(1)
0/0
def test_nothing():
pass
def teardown_module(module):
l.append(2)
""")
reprec.assertoutcome(failed=1)
calls = reprec.getcalls("pytest_runtest_setup")
assert calls[0].item.module.l == [1]
def test_setup_function_failure_no_teardown(testdir):
reprec = testdir.inline_runsource("""
modlevel = []
def setup_function(function):
modlevel.append(1)
0/0
def teardown_function(module):
modlevel.append(2)
def test_func():
pass
""")
calls = reprec.getcalls("pytest_runtest_setup")
assert calls[0].item.module.modlevel == [1]
def test_class_setup(testdir): def test_class_setup(testdir):
reprec = testdir.inline_runsource(""" reprec = testdir.inline_runsource("""
class TestSimpleClassSetup: class TestSimpleClassSetup:
@ -55,6 +88,23 @@ def test_class_setup(testdir):
""") """)
reprec.assertoutcome(passed=1+2+1) reprec.assertoutcome(passed=1+2+1)
def test_class_setup_failure_no_teardown(testdir):
reprec = testdir.inline_runsource("""
class TestSimpleClassSetup:
clslevel = []
def setup_class(cls):
0/0
def teardown_class(cls):
cls.clslevel.append(1)
def test_classlevel(self):
pass
def test_cleanup():
assert not TestSimpleClassSetup.clslevel
""")
reprec.assertoutcome(failed=1, passed=1)
def test_method_setup(testdir): def test_method_setup(testdir):
reprec = testdir.inline_runsource(""" reprec = testdir.inline_runsource("""
@ -72,6 +122,25 @@ def test_method_setup(testdir):
""") """)
reprec.assertoutcome(passed=2) reprec.assertoutcome(passed=2)
def test_method_setup_failure_no_teardown(testdir):
reprec = testdir.inline_runsource("""
class TestMethodSetup:
clslevel = []
def setup_method(self, method):
self.clslevel.append(1)
0/0
def teardown_method(self, method):
self.clslevel.append(2)
def test_method(self):
pass
def test_cleanup():
assert TestMethodSetup.clslevel == [1]
""")
reprec.assertoutcome(failed=1, passed=1)
def test_method_generator_setup(testdir): def test_method_generator_setup(testdir):
reprec = testdir.inline_runsource(""" reprec = testdir.inline_runsource("""
class TestSetupTeardownOnInstance: class TestSetupTeardownOnInstance:
@ -134,23 +203,7 @@ def test_method_setup_uses_fresh_instances(testdir):
""") """)
reprec.assertoutcome(passed=2, failed=0) reprec.assertoutcome(passed=2, failed=0)
def test_failing_setup_calls_teardown(testdir): def test_setup_that_skips_calledagain(testdir):
p = testdir.makepyfile("""
def setup_module(mod):
raise ValueError(42)
def test_function():
assert 0
def teardown_module(mod):
raise ValueError(43)
""")
result = testdir.runpytest(p)
result.stdout.fnmatch_lines([
"*42*",
"*43*",
"*2 error*"
])
def test_setup_that_skips_calledagain_and_teardown(testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile("""
import pytest import pytest
def setup_module(mod): def setup_module(mod):
@ -159,14 +212,9 @@ def test_setup_that_skips_calledagain_and_teardown(testdir):
pass pass
def test_function2(): def test_function2():
pass pass
def teardown_module(mod):
raise ValueError(43)
""") """)
result = testdir.runpytest(p) reprec = testdir.inline_run(p)
result.stdout.fnmatch_lines([ reprec.assertoutcome(skipped=2)
"*ValueError*43*",
"*2 skipped*1 error*",
])
def test_setup_fails_again_on_all_tests(testdir): def test_setup_fails_again_on_all_tests(testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile("""
@ -177,14 +225,9 @@ def test_setup_fails_again_on_all_tests(testdir):
pass pass
def test_function2(): def test_function2():
pass pass
def teardown_module(mod):
raise ValueError(43)
""") """)
result = testdir.runpytest(p) reprec = testdir.inline_run(p)
result.stdout.fnmatch_lines([ reprec.assertoutcome(failed=2)
"*3 error*"
])
assert "passed" not in result.stdout.str()
def test_setup_funcarg_setup_when_outer_scope_fails(testdir): def test_setup_funcarg_setup_when_outer_scope_fails(testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile("""
@ -207,6 +250,3 @@ def test_setup_funcarg_setup_when_outer_scope_fails(testdir):
"*2 error*" "*2 error*"
]) ])
assert "xyz43" not in result.stdout.str() assert "xyz43" not in result.stdout.str()

View File

@ -65,7 +65,7 @@ def test_setup(testdir):
rep = reprec.matchreport("test_both", when="teardown") rep = reprec.matchreport("test_both", when="teardown")
assert rep.failed and '42' in str(rep.longrepr) assert rep.failed and '42' in str(rep.longrepr)
def test_unittest_style_setup_teardown(testdir): def test_setUpModule(testdir):
testpath = testdir.makepyfile(""" testpath = testdir.makepyfile("""
l = [] l = []
@ -86,6 +86,23 @@ def test_unittest_style_setup_teardown(testdir):
"*2 passed*", "*2 passed*",
]) ])
def test_setUpModule_failing_no_teardown(testdir):
testpath = testdir.makepyfile("""
l = []
def setUpModule():
0/0
def tearDownModule():
l.append(1)
def test_hello():
pass
""")
reprec = testdir.inline_run(testpath)
reprec.assertoutcome(passed=0, failed=1)
call = reprec.getcalls("pytest_runtest_setup")[0]
assert not call.item.module.l
def test_new_instances(testdir): def test_new_instances(testdir):
testpath = testdir.makepyfile(""" testpath = testdir.makepyfile("""
@ -636,3 +653,4 @@ def test_no_teardown_if_setupclass_failed(testdir):
""") """)
reprec = testdir.inline_run(testpath) reprec = testdir.inline_run(testpath)
reprec.assertoutcome(passed=1, failed=1) reprec.assertoutcome(passed=1, failed=1)