From eda681af2b00a628fe2c747b5ec0ef90b4be5cbc Mon Sep 17 00:00:00 2001 From: Petter Strandmark Date: Thu, 19 Nov 2020 11:07:15 +0100 Subject: [PATCH] Call Python 3.8 doClassCleanups (#8033) --- AUTHORS | 1 + changelog/8032.improvement.rst | 1 + src/_pytest/unittest.py | 58 +++++++++--- testing/test_unittest.py | 160 +++++++++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 changelog/8032.improvement.rst diff --git a/AUTHORS b/AUTHORS index 8febe36ef..4b6786651 100644 --- a/AUTHORS +++ b/AUTHORS @@ -232,6 +232,7 @@ Pauli Virtanen Pavel Karateev Paweł Adamczak Pedro Algarvio +Petter Strandmark Philipp Loose Pieter Mulder Piotr Banaszkiewicz diff --git a/changelog/8032.improvement.rst b/changelog/8032.improvement.rst new file mode 100644 index 000000000..1aca72069 --- /dev/null +++ b/changelog/8032.improvement.rst @@ -0,0 +1 @@ +`doClassCleanups` (introduced in `unittest` in Python and 3.8) is now called. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 21db0ec23..55f15efe4 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -99,26 +99,48 @@ class UnitTestCase(Class): """Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding teardown functions (#517).""" class_fixture = _make_xunit_fixture( - cls, "setUpClass", "tearDownClass", scope="class", pass_self=False + cls, + "setUpClass", + "tearDownClass", + "doClassCleanups", + scope="class", + pass_self=False, ) if class_fixture: cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined] method_fixture = _make_xunit_fixture( - cls, "setup_method", "teardown_method", scope="function", pass_self=True + cls, + "setup_method", + "teardown_method", + None, + scope="function", + pass_self=True, ) if method_fixture: cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined] def _make_xunit_fixture( - obj: type, setup_name: str, teardown_name: str, scope: "_Scope", pass_self: bool + obj: type, + setup_name: str, + teardown_name: str, + cleanup_name: Optional[str], + scope: "_Scope", + pass_self: bool, ): setup = getattr(obj, setup_name, None) teardown = getattr(obj, teardown_name, None) if setup is None and teardown is None: return None + if cleanup_name: + cleanup = getattr(obj, cleanup_name, lambda *args: None) + else: + + def cleanup(*args): + pass + @pytest.fixture( scope=scope, autouse=True, @@ -130,16 +152,32 @@ def _make_xunit_fixture( reason = self.__unittest_skip_why__ pytest.skip(reason) if setup is not None: - if pass_self: - setup(self, request.function) - else: - setup() + try: + if pass_self: + setup(self, request.function) + else: + setup() + # unittest does not call the cleanup function for every BaseException, so we + # follow this here. + except Exception: + if pass_self: + cleanup(self) + else: + cleanup() + + raise yield - if teardown is not None: + try: + if teardown is not None: + if pass_self: + teardown(self, request.function) + else: + teardown() + finally: if pass_self: - teardown(self, request.function) + cleanup(self) else: - teardown() + cleanup() return fixture diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 2c8d03cb9..8b00cb826 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1260,3 +1260,163 @@ def test_plain_unittest_does_not_support_async(testdir): "*1 passed*", ] result.stdout.fnmatch_lines(expected_lines) + + +@pytest.mark.skipif( + sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" +) +def test_do_class_cleanups_on_success(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + @classmethod + def setUpClass(cls): + def cleanup(): + cls.values.append(1) + cls.addClassCleanup(cleanup) + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_exactly_once(): + assert MyTestCase.values == [1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 0 + assert passed == 3 + + +@pytest.mark.skipif( + sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" +) +def test_do_class_cleanups_on_setupclass_failure(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + @classmethod + def setUpClass(cls): + def cleanup(): + cls.values.append(1) + cls.addClassCleanup(cleanup) + assert False + def test_one(self): + pass + def test_cleanup_called_exactly_once(): + assert MyTestCase.values == [1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 1 + assert passed == 1 + + +@pytest.mark.skipif( + sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" +) +def test_do_class_cleanups_on_teardownclass_failure(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + @classmethod + def setUpClass(cls): + def cleanup(): + cls.values.append(1) + cls.addClassCleanup(cleanup) + @classmethod + def tearDownClass(cls): + assert False + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_exactly_once(): + assert MyTestCase.values == [1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert passed == 3 + + +def test_do_cleanups_on_success(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + def setUp(self): + def cleanup(): + self.values.append(1) + self.addCleanup(cleanup) + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_the_right_number_of_times(): + assert MyTestCase.values == [1, 1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 0 + assert passed == 3 + + +def test_do_cleanups_on_setup_failure(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + def setUp(self): + def cleanup(): + self.values.append(1) + self.addCleanup(cleanup) + assert False + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_the_right_number_of_times(): + assert MyTestCase.values == [1, 1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 2 + assert passed == 1 + + +def test_do_cleanups_on_teardown_failure(testdir): + testpath = testdir.makepyfile( + """ + import unittest + class MyTestCase(unittest.TestCase): + values = [] + def setUp(self): + def cleanup(): + self.values.append(1) + self.addCleanup(cleanup) + def tearDown(self): + assert False + def test_one(self): + pass + def test_two(self): + pass + def test_cleanup_called_the_right_number_of_times(): + assert MyTestCase.values == [1, 1] + """ + ) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 2 + assert passed == 1