diff --git a/changes/237.bugfix.rst b/changes/237.bugfix.rst new file mode 100644 index 0000000..9515a69 --- /dev/null +++ b/changes/237.bugfix.rst @@ -0,0 +1 @@ +Ensure teardown reports are passed to ``pytest_runtest_logreport`` for rerun attempts. diff --git a/src/pytest_rerunfailures.py b/src/pytest_rerunfailures.py index a8c04ad..be3c7ca 100644 --- a/src/pytest_rerunfailures.py +++ b/src/pytest_rerunfailures.py @@ -966,10 +966,11 @@ def pytest_runtest_protocol(item, nextitem): item.ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) reports = runtestprotocol(item, nextitem=nextitem, log=False) + rerun_triggered = False for report in reports: # 3 reports: setup, call, teardown report.rerun = item.execution_count - 1 - if _should_not_rerun(item, report, reruns): - # last run or no failure detected, log normally + if rerun_triggered or _should_not_rerun(item, report, reruns): + # no rerun needed or one already triggered, log normally item.ihook.pytest_runtest_logreport(report=report) else: # failure detected and reruns not exhausted, since i < reruns @@ -995,9 +996,9 @@ def pytest_runtest_protocol(item, nextitem): _remove_failed_subtests_from_report(item, report) _remove_failed_subtest_reports_from_stats(item) - break # trigger rerun - else: - need_to_run = False + rerun_triggered = True + + need_to_run = rerun_triggered item.ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) diff --git a/tests/test_pytest_rerunfailures.py b/tests/test_pytest_rerunfailures.py index 8c1ba80..ae750da 100644 --- a/tests/test_pytest_rerunfailures.py +++ b/tests/test_pytest_rerunfailures.py @@ -863,6 +863,66 @@ def pytest_runtest_logreport(report): assert_outcomes(result, failed=1, rerun=2, passed=0) +def test_rerun_report_includes_teardown_from_each_attempt(testdir): + testdir.makepyfile("def test_fail(): assert False") + testdir.makeconftest( + """ + reports = [] + + def pytest_runtest_logreport(report): + reports.append((report.when, report.outcome, report.rerun)) + + def pytest_sessionfinish(): + print(f"REPORTS: {reports!r}") + """ + ) + + result = testdir.runpytest("--reruns", "1") + + assert_outcomes(result, failed=1, rerun=1, passed=0) + expected_reports = ( + "REPORTS: [('setup', 'passed', 0), ('call', 'rerun', 0), " + "('teardown', 'passed', 0), ('setup', 'passed', 1), " + "('call', 'failed', 1), ('teardown', 'passed', 1)]" + ) + assert expected_reports in result.stdout.str() + + +def test_single_attempt_triggers_at_most_one_rerun(testdir): + testdir.makepyfile("def test_fail(failing_teardown): assert False") + testdir.makeconftest( + """ + import pytest + + attempts = 0 + first_attempt_reports = [] + + @pytest.fixture + def failing_teardown(): + global attempts + attempts += 1 + yield + raise RuntimeError("teardown failure") + + def pytest_runtest_logreport(report): + if report.rerun == 0 and report.when in ("call", "teardown"): + first_attempt_reports.append((report.when, report.outcome)) + + def pytest_sessionfinish(): + print(f"ATTEMPTS: {attempts}") + print(f"FIRST ATTEMPT REPORTS: {first_attempt_reports!r}") + """ + ) + + result = testdir.runpytest("--reruns", "1") + + stdout = result.stdout.str() + assert "ATTEMPTS: 2" in stdout + assert ( + "FIRST ATTEMPT REPORTS: [('call', 'rerun'), ('teardown', 'failed')]" in stdout + ) + + def test_pytest_runtest_logfinish_is_called(testdir): hook_message = "Message from pytest_runtest_logfinish hook" testdir.makepyfile("def test_pass(): pass")