Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ Bug fixes
teardown was ignored and the test was rerun anyway.
Fixes `#270 <https://github.com/pytest-dev/pytest-rerunfailures/issues/270>`_.

- Create a new test class instance for each rerun. Previously the instance of
the failed attempt was reused, so state stored on ``self`` leaked into the
rerun and broke test isolation. Fixtures cached at class scope or higher are
still not re-executed.
Fixes `#268 <https://github.com/pytest-dev/pytest-rerunfailures/issues/268>`_.

Features
++++++++

Expand Down
19 changes: 19 additions & 0 deletions src/pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,24 @@ def _remove_cached_results_from_failed_fixtures(item):
fixture_def._finalizers.clear()


def _discard_test_class_instance(item):
"""
Drop the cached test class instance so the rerun gets a fresh one.

Note: pytest creates one instance per test item and caches it on the item,
so without this the rerun would reuse the instance -- and therefore any
state stored on ``self`` -- of the attempt that just failed.
"""
if getattr(item, "_instance", None) is None:
# no instance is cached: not a test method, or pytest dropped it itself
return

# ``obj`` is the method bound to the cached instance, so it has to go too;
# both are recomputed lazily on next access.
del item._instance
item._obj = None


def _remove_failed_setup_state_from_session(item):
"""
Clean up setup state.
Expand Down Expand Up @@ -947,6 +965,7 @@ def pytest_runtest_protocol(item, nextitem):
# cleanin item's cashed results from any level of setups
_remove_cached_results_from_failed_fixtures(item)
_remove_failed_setup_state_from_session(item)
_discard_test_class_instance(item)
_remove_failed_subtests_from_report(item, report)
_remove_failed_subtest_reports_from_stats(item)

Expand Down
69 changes: 69 additions & 0 deletions tests/test_pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,75 @@ def test_pass_2(self, session_fixture, setup_fixture):
assert_outcomes(result, passed=2, rerun=1)


def test_rerun_recreates_test_class_instance(testdir):
"""
Case: state stored on ``self`` by a failed attempt must not leak into the
rerun, i.e. every attempt gets a fresh test class instance
"""
testdir.makepyfile(
"""
import pytest

attempts = 0

class TestFoo(object):
@pytest.fixture(autouse=True)
def counting_fixture(self):
assert not hasattr(self, 'seen_by_fixture')
self.seen_by_fixture = True

@pytest.mark.flaky(reruns=2)
def test_fresh_instance(self):
global attempts
attempts += 1
assert not hasattr(self, 'seen_by_test')
self.seen_by_test = True
assert attempts == 3"""
)
result = testdir.runpytest()
assert_outcomes(result, passed=1, rerun=2)


def test_rerun_recreates_instance_without_re_executing_scoped_fixtures(testdir):
"""
Case: recreating the test class instance for a rerun must not invalidate
fixtures cached at a higher scope than function
"""
testdir.makepyfile(
"""
import pytest

attempts = 0
executions = {'session': 0, 'module': 0, 'class': 0}

@pytest.fixture(scope='session')
def session_fixture():
executions['session'] += 1

@pytest.fixture(scope='module')
def module_fixture():
executions['module'] += 1

@pytest.fixture(scope='class')
def class_fixture():
executions['class'] += 1

class TestFoo(object):
@pytest.mark.flaky(reruns=2)
def test_fresh_instance(
self, session_fixture, module_fixture, class_fixture
):
global attempts
attempts += 1
assert executions == {'session': 1, 'module': 1, 'class': 1}
assert not hasattr(self, 'seen_by_test')
self.seen_by_test = True
assert attempts == 3"""
)
result = testdir.runpytest()
assert_outcomes(result, passed=1, rerun=2)


def test_execution_count_exposed(testdir):
testdir.makepyfile("def test_pass(): assert True")
testdir.makeconftest(
Expand Down
Loading