Skip to content
Open
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
1 change: 1 addition & 0 deletions changes/357.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Keep module, class, and session scoped fixtures alive across re-runs of a test whose call phase failed through subtests only.
14 changes: 9 additions & 5 deletions src/pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ def is_matching_subtest_report(report):
_remove_subtest_reports("subtests passed")


def _get_num_failed_subtests(item, report):
def _get_num_failed_subtests(item, nodeid):
"""
Return the number of failed subtests.

Expand All @@ -492,7 +492,7 @@ def _get_num_failed_subtests(item, report):

failed_subtests = item.config.stash.get(failed_subtests_key, None)
if failed_subtests is not None:
return failed_subtests.get(report.nodeid, 0)
return failed_subtests.get(nodeid, 0)

return 0

Expand Down Expand Up @@ -563,7 +563,7 @@ def _should_not_rerun(item, report, reruns):
is_terminal_error = any(item._terminal_errors.values())
condition = get_reruns_condition(item)
has_failed_subtests = (
report.when == "call" and _get_num_failed_subtests(item, report) > 0
report.when == "call" and _get_num_failed_subtests(item, report.nodeid) > 0
)

return (
Expand Down Expand Up @@ -923,10 +923,14 @@ def pytest_runtest_teardown(item, nextitem):
# Only remove non-function level actions from the stack if the test is to be re-run
# Exceeding re-run limits, being free of failue statuses, encountering
# allowable exceptions, and a falsy flaky condition indicate that the test is
# not to be re-ran.
# not to be re-ran. A failure can also be carried by failed subtests alone,
# which leaves the call phase itself passing.
if (
item.execution_count <= reruns
and any(_test_failed_statuses.values())
and (
any(_test_failed_statuses.values())
or _get_num_failed_subtests(item, item.nodeid) > 0
)
and not any(item._terminal_errors.values())
and get_reruns_condition(item)
):
Expand Down
73 changes: 73 additions & 0 deletions tests/test_pytest_rerunfailures.py
Original file line number Diff line number Diff line change
Expand Up @@ -2254,6 +2254,79 @@ def test_subtests(subtests):
assert_outcomes(result, passed=0, failed=2, rerun=1)


@pytest.mark.skipif(not has_subtests, reason="Only supported on pytest 9.0 and newer")
@pytest.mark.parametrize("scope", ["class", "module", "session"])
def test_failing_subtests_keep_higher_scope_fixture_alive(testdir, scope):
testdir.makepyfile(
f"""
import pytest

@pytest.fixture(scope="{scope}", autouse=True)
def higher_scope_fixture():
yield
print("{scope} teardown")

class TestSubtests:
def test_subtests(self, subtests):
with subtests.test("Fails on first attempt"):
{indent(temporary_failure(), " ")}
"""
)

result = testdir.runpytest("-s", "--reruns", "1")
assert_outcomes(result, passed=1, rerun=1)
assert result.stdout.str().count(f"{scope} teardown") == 1


@pytest.mark.skipif(not has_subtests, reason="Only supported on pytest 9.0 and newer")
def test_failing_subtests_keep_earlier_module_fixture_alive(testdir):
testdir.makepyfile(
test_flaky_subtests_module=f"""
import pytest

@pytest.fixture(scope="module", autouse=True)
def subtests_module_fixture():
yield
print("subtests module teardown")

def test_subtests(subtests):
with subtests.test("Fails on first attempt"):
{indent(temporary_failure(), " ")}""",
test_later_module="""
def test_pass():
print("later module test")""",
)

result = testdir.runpytest("-s", "--reruns", "1")
assert_outcomes(result, passed=2, rerun=1)
assert result.stdout.str().count("subtests module teardown") == 1
result.stdout.fnmatch_lines(
["*subtests module teardown*", "*later module test*"],
)


@pytest.mark.skipif(not has_subtests, reason="Only supported on pytest 9.0 and newer")
def test_too_many_failing_subtests_tear_down_module_fixture_once(testdir):
testdir.makepyfile(
"""
import pytest

@pytest.fixture(scope="module", autouse=True)
def module_fixture():
yield
print("module teardown")

def test_subtests(subtests):
with subtests.test("Always fails"):
assert False
"""
)

result = testdir.runpytest("-s", "--reruns", "1")
assert_outcomes(result, passed=0, failed=2, rerun=1)
assert result.stdout.str().count("module teardown") == 1


@pytest.mark.skipif(
not has_subtests or not has_xdist,
reason="Requires pytest 9.0 or newer and xdist",
Expand Down
Loading