diff --git a/AUTHORS b/AUTHORS index 136ffd6c5ee..ec70ec8adba 100644 --- a/AUTHORS +++ b/AUTHORS @@ -375,6 +375,7 @@ Nipunn Koorapati Oleg Pidsadnyi Oleg Sushchenko Oleksandr Zavertniev +Oleksii Ovdiienko Olga Matoula Oliver Bestwalter Olivier Grisel diff --git a/changelog/15316.bugfix.rst b/changelog/15316.bugfix.rst new file mode 100644 index 00000000000..7a12ec0c79e --- /dev/null +++ b/changelog/15316.bugfix.rst @@ -0,0 +1 @@ +:hook:`pytest_runtest_logfinish` is now called even when a test is interrupted by :func:`pytest.exit` or :class:`KeyboardInterrupt`, so it is always paired with :hook:`pytest_runtest_logstart`. diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 629796598df..3ec550fe841 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -115,8 +115,12 @@ def pytest_sessionfinish(session: Session) -> None: def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> bool: ihook = item.ihook ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) - runtestprotocol(item, nextitem=nextitem) - ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) + try: + runtestprotocol(item, nextitem=nextitem) + finally: + # Exit and KeyboardInterrupt propagate out of runtestprotocol (see + # get_reraise_exceptions); still pair every logstart with a logfinish. + ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location) return True diff --git a/testing/test_runner.py b/testing/test_runner.py index 8b59a5ee8c1..10fb1588f7a 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -339,6 +339,36 @@ def test_func(): assert rep.nodeid == "test_logstart_logfinish_hooks.py::test_func" assert rep.location == ("test_logstart_logfinish_hooks.py", 1, "test_func") + @pytest.mark.parametrize( + "raise_stmt", ["pytest.exit('stop')", "raise KeyboardInterrupt"] + ) + def test_logfinish_called_on_reraised_exception( + self, pytester: Pytester, raise_stmt: str + ) -> None: + """pytest_runtest_logfinish is still called when the test protocol is + aborted by an exception which is reraised (Exit, KeyboardInterrupt).""" + pytester.makepyfile( + f""" + import pytest + def test_func(): + {raise_stmt} + def test_not_run(): + pass + """ + ) + rec = pytester.inline_run(no_reraise_ctrlc=True) + reps = rec.getcalls("pytest_runtest_logstart pytest_runtest_logfinish") + assert [(x._name, x.nodeid) for x in reps] == [ + ( + "pytest_runtest_logstart", + "test_logfinish_called_on_reraised_exception.py::test_func", + ), + ( + "pytest_runtest_logfinish", + "test_logfinish_called_on_reraised_exception.py::test_func", + ), + ] + def test_exact_teardown_issue90(self, pytester: Pytester) -> None: rec = pytester.inline_runsource( """