Skip to content

Commit cb1234d

Browse files
lukasNebricemac
andauthored
Fix crash in runtest teardown hook (#207)
* avoid crash during teardown when runtest protocol hook was not executed * avoid crash during teardown when TestCase class is used as base class Co-authored-by: Michael Howitz <icemac@gmx.net>
1 parent b20fef5 commit cb1234d

3 files changed

Lines changed: 175 additions & 26 deletions

File tree

CHANGES.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
Changelog
22
=========
33

4-
11.1 (unreleased)
4+
11.1.1 (unreleased)
5+
-------------------
6+
7+
Bug fixes
8+
+++++++++
9+
10+
- Fix crash during teardown when runtest protocol hook is overwritten by another plugin.
11+
- Fix crash during teardown when TestCase class is used as base class.
12+
13+
11.1 (2023-02-09)
514
-----------------
615

716
Bug fixes

pytest_rerunfailures.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import pytest
1414
from _pytest.outcomes import fail
15-
from _pytest.python import Function
1615
from _pytest.runner import runtestprotocol
1716
from packaging.version import parse as parse_version
1817

@@ -513,23 +512,29 @@ def pytest_runtest_teardown(item, nextitem):
513512
# flaky
514513
return
515514

515+
if not hasattr(item, "execution_count"):
516+
# pytest_runtest_protocol hook of this plugin was not executed
517+
# -> teardown needs to be skipped as well
518+
return
519+
516520
# teardown when test not failed or rerun limit exceeded
517521
if item.execution_count > reruns or getattr(item, "test_failed", None) is False:
518522
item.teardown()
519523
else:
520524
# clean cashed results from any level of setups
521525
_remove_cached_results_from_failed_fixtures(item)
522526

523-
if PYTEST_GTE_63:
524-
for key in list(item.session._setupstate.stack.keys()):
525-
if type(key) != Function:
526-
del item.session._setupstate.stack[key]
527-
else:
528-
for node in list(item.session._setupstate.stack):
529-
if type(node) != Function:
530-
item.session._setupstate.stack.remove(node)
527+
if item in item.session._setupstate.stack:
528+
if PYTEST_GTE_63:
529+
for key in list(item.session._setupstate.stack.keys()):
530+
if key != item:
531+
del item.session._setupstate.stack[key]
532+
else:
533+
for node in list(item.session._setupstate.stack):
534+
if node != item:
535+
item.session._setupstate.stack.remove(node)
531536

532-
item.teardown()
537+
item.teardown()
533538

534539

535540
@pytest.hookimpl(hookwrapper=True)

test_pytest_rerunfailures.py

Lines changed: 150 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -709,45 +709,109 @@ def test_run_session_teardown_once_after_reruns(testdir):
709709
import logging
710710
import pytest
711711
712-
@pytest.fixture(scope='session')
712+
from unittest import TestCase
713+
714+
@pytest.fixture(scope='session', autouse=True)
713715
def session_fixture():
714716
logging.info('session setup')
715717
yield
716718
logging.info('session teardown')
717719
718-
@pytest.fixture(scope='class')
720+
@pytest.fixture(scope='class', autouse=True)
719721
def class_fixture():
720722
logging.info('class setup')
721723
yield
722724
logging.info('class teardown')
723725
724-
@pytest.fixture(scope='function')
726+
@pytest.fixture(scope='function', autouse=True)
725727
def function_fixture():
726728
logging.info('function setup')
727729
yield
728730
logging.info('function teardown')
729731
730-
class TestFoo:
732+
class TestFirstPassLastFail:
733+
734+
@staticmethod
735+
def test_1():
736+
logging.info("TestFirstPassLastFail 1")
737+
731738
@staticmethod
732-
def test_foo_1(session_fixture, class_fixture, function_fixture):
733-
pass
739+
def test_2():
740+
logging.info("TestFirstPassLastFail 2")
741+
assert False
742+
743+
class TestFirstFailLastPass:
734744
735745
@staticmethod
736-
def test_foo_2(session_fixture, class_fixture, function_fixture):
746+
def test_1():
747+
logging.info("TestFirstFailLastPass 1")
737748
assert False
738749
739-
class TestBar:
740750
@staticmethod
741-
def test_bar_1(session_fixture, class_fixture, function_fixture):
751+
def test_2():
752+
logging.info("TestFirstFailLastPass 2")
753+
754+
class TestSkipFirst:
755+
@staticmethod
756+
@pytest.mark.skipif(True, reason='Some reason')
757+
def test_1():
758+
logging.info("TestSkipFirst 1")
742759
assert False
743760
744761
@staticmethod
745-
def test_bar_2(session_fixture, class_fixture, function_fixture):
762+
def test_2():
763+
logging.info("TestSkipFirst 2")
746764
assert False
747765
766+
class TestSkipLast:
748767
@staticmethod
749-
def test_bar_3(session_fixture, class_fixture, function_fixture):
750-
pass"""
768+
def test_1():
769+
logging.info("TestSkipLast 1")
770+
assert False
771+
772+
@staticmethod
773+
@pytest.mark.skipif(True, reason='Some reason')
774+
def test_2():
775+
logging.info("TestSkipLast 2")
776+
assert False
777+
778+
class TestTestCaseFailFirstFailLast(TestCase):
779+
780+
@staticmethod
781+
def test_1():
782+
logging.info("TestTestCaseFailFirstFailLast 1")
783+
assert False
784+
785+
@staticmethod
786+
def test_2():
787+
logging.info("TestTestCaseFailFirstFailLast 2")
788+
assert False
789+
790+
class TestTestCaseSkipFirst(TestCase):
791+
792+
@staticmethod
793+
@pytest.mark.skipif(True, reason='Some reason')
794+
def test_1():
795+
logging.info("TestTestCaseSkipFirst 1")
796+
assert False
797+
798+
@staticmethod
799+
def test_2():
800+
logging.info("TestTestCaseSkipFirst 2")
801+
assert False
802+
803+
class TestTestCaseSkipLast(TestCase):
804+
805+
@staticmethod
806+
def test_1():
807+
logging.info("TestTestCaseSkipLast 1")
808+
assert False
809+
810+
@staticmethod
811+
@pytest.mark.skipif(True, reason="Some reason")
812+
def test_2():
813+
logging.info("TestTestCaseSkipLast 2")
814+
assert False"""
751815
)
752816
import logging
753817

@@ -756,36 +820,107 @@ def test_bar_3(session_fixture, class_fixture, function_fixture):
756820
result = testdir.runpytest("--reruns", "2")
757821
expected_calls = [
758822
mock.call("session setup"),
759-
# class TestFoo
823+
# TestFirstPassLastFail
824+
mock.call("class setup"),
825+
mock.call("function setup"),
826+
mock.call("TestFirstPassLastFail 1"),
827+
mock.call("function teardown"),
828+
mock.call("function setup"),
829+
mock.call("TestFirstPassLastFail 2"),
830+
mock.call("function teardown"),
831+
mock.call("function setup"),
832+
mock.call("TestFirstPassLastFail 2"),
833+
mock.call("function teardown"),
834+
mock.call("function setup"),
835+
mock.call("TestFirstPassLastFail 2"),
836+
mock.call("function teardown"),
837+
mock.call("class teardown"),
838+
# TestFirstFailLastPass
760839
mock.call("class setup"),
761840
mock.call("function setup"),
841+
mock.call("TestFirstFailLastPass 1"),
762842
mock.call("function teardown"),
763843
mock.call("function setup"),
844+
mock.call("TestFirstFailLastPass 1"),
764845
mock.call("function teardown"),
765846
mock.call("function setup"),
847+
mock.call("TestFirstFailLastPass 1"),
766848
mock.call("function teardown"),
767849
mock.call("function setup"),
850+
mock.call("TestFirstFailLastPass 2"),
768851
mock.call("function teardown"),
769852
mock.call("class teardown"),
770-
# class TestBar
853+
# TestSkipFirst
771854
mock.call("class setup"),
772855
mock.call("function setup"),
856+
mock.call("TestSkipFirst 2"),
773857
mock.call("function teardown"),
774858
mock.call("function setup"),
859+
mock.call("TestSkipFirst 2"),
775860
mock.call("function teardown"),
776861
mock.call("function setup"),
862+
mock.call("TestSkipFirst 2"),
863+
mock.call("function teardown"),
864+
mock.call("class teardown"),
865+
# TestSkipLast
866+
mock.call("class setup"),
867+
mock.call("function setup"),
868+
mock.call("TestSkipLast 1"),
869+
mock.call("function teardown"),
870+
mock.call("function setup"),
871+
mock.call("TestSkipLast 1"),
872+
mock.call("function teardown"),
873+
mock.call("function setup"),
874+
mock.call("TestSkipLast 1"),
875+
mock.call("function teardown"),
876+
mock.call("class teardown"),
877+
# TestTestCaseFailFirstFailLast
878+
mock.call("class setup"),
879+
mock.call("function setup"),
880+
mock.call("TestTestCaseFailFirstFailLast 1"),
777881
mock.call("function teardown"),
778882
mock.call("function setup"),
883+
mock.call("TestTestCaseFailFirstFailLast 1"),
779884
mock.call("function teardown"),
780885
mock.call("function setup"),
886+
mock.call("TestTestCaseFailFirstFailLast 1"),
887+
mock.call("function teardown"),
888+
mock.call("function setup"),
889+
mock.call("TestTestCaseFailFirstFailLast 2"),
890+
mock.call("function teardown"),
891+
mock.call("function setup"),
892+
mock.call("TestTestCaseFailFirstFailLast 2"),
893+
mock.call("function teardown"),
894+
mock.call("function setup"),
895+
mock.call("TestTestCaseFailFirstFailLast 2"),
896+
mock.call("function teardown"),
897+
mock.call("class teardown"),
898+
# TestTestCaseSkipFirst
899+
mock.call("class setup"),
900+
mock.call("function setup"),
901+
mock.call("TestTestCaseSkipFirst 2"),
902+
mock.call("function teardown"),
903+
mock.call("function setup"),
904+
mock.call("TestTestCaseSkipFirst 2"),
905+
mock.call("function teardown"),
906+
mock.call("function setup"),
907+
mock.call("TestTestCaseSkipFirst 2"),
908+
mock.call("function teardown"),
909+
mock.call("class teardown"),
910+
# TestTestCaseSkipLast
911+
mock.call("class setup"),
912+
mock.call("function setup"),
913+
mock.call("TestTestCaseSkipLast 1"),
781914
mock.call("function teardown"),
782915
mock.call("function setup"),
916+
mock.call("TestTestCaseSkipLast 1"),
783917
mock.call("function teardown"),
784918
mock.call("function setup"),
919+
mock.call("TestTestCaseSkipLast 1"),
785920
mock.call("function teardown"),
786921
mock.call("class teardown"),
787922
mock.call("session teardown"),
788923
]
789924

790925
logging.info.assert_has_calls(expected_calls, any_order=False)
791-
assert_outcomes(result, failed=3, passed=2, rerun=6)
926+
assert_outcomes(result, failed=8, passed=2, rerun=16, skipped=4)

0 commit comments

Comments
 (0)