Skip to content

Commit

Permalink
Fix crash in runtest teardown hook (#207)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
lukasNebr and icemac authored Feb 17, 2023
1 parent b20fef5 commit cb1234d
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 26 deletions.
11 changes: 10 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
Changelog
=========

11.1 (unreleased)
11.1.1 (unreleased)
-------------------

Bug fixes
+++++++++

- Fix crash during teardown when runtest protocol hook is overwritten by another plugin.
- Fix crash during teardown when TestCase class is used as base class.

11.1 (2023-02-09)
-----------------

Bug fixes
Expand Down
25 changes: 15 additions & 10 deletions pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import pytest
from _pytest.outcomes import fail
from _pytest.python import Function
from _pytest.runner import runtestprotocol
from packaging.version import parse as parse_version

Expand Down Expand Up @@ -513,23 +512,29 @@ def pytest_runtest_teardown(item, nextitem):
# flaky
return

if not hasattr(item, "execution_count"):
# pytest_runtest_protocol hook of this plugin was not executed
# -> teardown needs to be skipped as well
return

# teardown when test not failed or rerun limit exceeded
if item.execution_count > reruns or getattr(item, "test_failed", None) is False:
item.teardown()
else:
# clean cashed results from any level of setups
_remove_cached_results_from_failed_fixtures(item)

if PYTEST_GTE_63:
for key in list(item.session._setupstate.stack.keys()):
if type(key) != Function:
del item.session._setupstate.stack[key]
else:
for node in list(item.session._setupstate.stack):
if type(node) != Function:
item.session._setupstate.stack.remove(node)
if item in item.session._setupstate.stack:
if PYTEST_GTE_63:
for key in list(item.session._setupstate.stack.keys()):
if key != item:
del item.session._setupstate.stack[key]
else:
for node in list(item.session._setupstate.stack):
if node != item:
item.session._setupstate.stack.remove(node)

item.teardown()
item.teardown()


@pytest.hookimpl(hookwrapper=True)
Expand Down
165 changes: 150 additions & 15 deletions test_pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,45 +709,109 @@ def test_run_session_teardown_once_after_reruns(testdir):
import logging
import pytest
@pytest.fixture(scope='session')
from unittest import TestCase
@pytest.fixture(scope='session', autouse=True)
def session_fixture():
logging.info('session setup')
yield
logging.info('session teardown')
@pytest.fixture(scope='class')
@pytest.fixture(scope='class', autouse=True)
def class_fixture():
logging.info('class setup')
yield
logging.info('class teardown')
@pytest.fixture(scope='function')
@pytest.fixture(scope='function', autouse=True)
def function_fixture():
logging.info('function setup')
yield
logging.info('function teardown')
class TestFoo:
class TestFirstPassLastFail:
@staticmethod
def test_1():
logging.info("TestFirstPassLastFail 1")
@staticmethod
def test_foo_1(session_fixture, class_fixture, function_fixture):
pass
def test_2():
logging.info("TestFirstPassLastFail 2")
assert False
class TestFirstFailLastPass:
@staticmethod
def test_foo_2(session_fixture, class_fixture, function_fixture):
def test_1():
logging.info("TestFirstFailLastPass 1")
assert False
class TestBar:
@staticmethod
def test_bar_1(session_fixture, class_fixture, function_fixture):
def test_2():
logging.info("TestFirstFailLastPass 2")
class TestSkipFirst:
@staticmethod
@pytest.mark.skipif(True, reason='Some reason')
def test_1():
logging.info("TestSkipFirst 1")
assert False
@staticmethod
def test_bar_2(session_fixture, class_fixture, function_fixture):
def test_2():
logging.info("TestSkipFirst 2")
assert False
class TestSkipLast:
@staticmethod
def test_bar_3(session_fixture, class_fixture, function_fixture):
pass"""
def test_1():
logging.info("TestSkipLast 1")
assert False
@staticmethod
@pytest.mark.skipif(True, reason='Some reason')
def test_2():
logging.info("TestSkipLast 2")
assert False
class TestTestCaseFailFirstFailLast(TestCase):
@staticmethod
def test_1():
logging.info("TestTestCaseFailFirstFailLast 1")
assert False
@staticmethod
def test_2():
logging.info("TestTestCaseFailFirstFailLast 2")
assert False
class TestTestCaseSkipFirst(TestCase):
@staticmethod
@pytest.mark.skipif(True, reason='Some reason')
def test_1():
logging.info("TestTestCaseSkipFirst 1")
assert False
@staticmethod
def test_2():
logging.info("TestTestCaseSkipFirst 2")
assert False
class TestTestCaseSkipLast(TestCase):
@staticmethod
def test_1():
logging.info("TestTestCaseSkipLast 1")
assert False
@staticmethod
@pytest.mark.skipif(True, reason="Some reason")
def test_2():
logging.info("TestTestCaseSkipLast 2")
assert False"""
)
import logging

Expand All @@ -756,36 +820,107 @@ def test_bar_3(session_fixture, class_fixture, function_fixture):
result = testdir.runpytest("--reruns", "2")
expected_calls = [
mock.call("session setup"),
# class TestFoo
# TestFirstPassLastFail
mock.call("class setup"),
mock.call("function setup"),
mock.call("TestFirstPassLastFail 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestFirstPassLastFail 2"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestFirstPassLastFail 2"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestFirstPassLastFail 2"),
mock.call("function teardown"),
mock.call("class teardown"),
# TestFirstFailLastPass
mock.call("class setup"),
mock.call("function setup"),
mock.call("TestFirstFailLastPass 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestFirstFailLastPass 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestFirstFailLastPass 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestFirstFailLastPass 2"),
mock.call("function teardown"),
mock.call("class teardown"),
# class TestBar
# TestSkipFirst
mock.call("class setup"),
mock.call("function setup"),
mock.call("TestSkipFirst 2"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestSkipFirst 2"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestSkipFirst 2"),
mock.call("function teardown"),
mock.call("class teardown"),
# TestSkipLast
mock.call("class setup"),
mock.call("function setup"),
mock.call("TestSkipLast 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestSkipLast 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestSkipLast 1"),
mock.call("function teardown"),
mock.call("class teardown"),
# TestTestCaseFailFirstFailLast
mock.call("class setup"),
mock.call("function setup"),
mock.call("TestTestCaseFailFirstFailLast 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestTestCaseFailFirstFailLast 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestTestCaseFailFirstFailLast 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestTestCaseFailFirstFailLast 2"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestTestCaseFailFirstFailLast 2"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestTestCaseFailFirstFailLast 2"),
mock.call("function teardown"),
mock.call("class teardown"),
# TestTestCaseSkipFirst
mock.call("class setup"),
mock.call("function setup"),
mock.call("TestTestCaseSkipFirst 2"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestTestCaseSkipFirst 2"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestTestCaseSkipFirst 2"),
mock.call("function teardown"),
mock.call("class teardown"),
# TestTestCaseSkipLast
mock.call("class setup"),
mock.call("function setup"),
mock.call("TestTestCaseSkipLast 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestTestCaseSkipLast 1"),
mock.call("function teardown"),
mock.call("function setup"),
mock.call("TestTestCaseSkipLast 1"),
mock.call("function teardown"),
mock.call("class teardown"),
mock.call("session teardown"),
]

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

0 comments on commit cb1234d

Please sign in to comment.