Skip to content

Commit 075c5ef

Browse files
RonnyPfannschmidtgraingertpre-commit-ci[bot]
authored
use Contextmanagers to handle StopIteration in generators (#12934)
* prepare example test for stopiteration passover issue * WIP: use contextmanagers instead of yield from as it turns out, StopIteration is not transparent on the boundaries of generators # Conflicts: # src/_pytest/threadexception.py # src/_pytest/unraisableexception.py * fixup rebase * rebase fixup more * add tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove hook_exceptions examples - these are tests now * add changelog * handle different numbers of === * handle different numbers of !!! --------- Co-authored-by: Thomas Grainger <tagrain@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent ca8b6f2 commit 075c5ef

File tree

3 files changed

+54
-3
lines changed

3 files changed

+54
-3
lines changed

changelog/12929.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Handle StopIteration from test cases, setup and teardown correctly.

src/_pytest/logging.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@ def pytest_runtest_logstart(self) -> None:
809809
def pytest_runtest_logreport(self) -> None:
810810
self.log_cli_handler.set_when("logreport")
811811

812+
@contextmanager
812813
def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None]:
813814
"""Implement the internals of the pytest_runtest_xxx() hooks."""
814815
with (
@@ -838,20 +839,23 @@ def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None]:
838839

839840
empty: dict[str, list[logging.LogRecord]] = {}
840841
item.stash[caplog_records_key] = empty
841-
yield from self._runtest_for(item, "setup")
842+
with self._runtest_for(item, "setup"):
843+
yield
842844

843845
@hookimpl(wrapper=True)
844846
def pytest_runtest_call(self, item: nodes.Item) -> Generator[None]:
845847
self.log_cli_handler.set_when("call")
846848

847-
yield from self._runtest_for(item, "call")
849+
with self._runtest_for(item, "call"):
850+
yield
848851

849852
@hookimpl(wrapper=True)
850853
def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None]:
851854
self.log_cli_handler.set_when("teardown")
852855

853856
try:
854-
yield from self._runtest_for(item, "teardown")
857+
with self._runtest_for(item, "teardown"):
858+
yield
855859
finally:
856860
del item.stash[caplog_records_key]
857861
del item.stash[caplog_handler_key]

testing/acceptance_test.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,3 +1606,49 @@ def test_no_terminal_plugin(pytester: Pytester) -> None:
16061606
pytester.makepyfile("def test(): assert 1 == 2")
16071607
result = pytester.runpytest("-pno:terminal", "-s")
16081608
assert result.ret == ExitCode.TESTS_FAILED
1609+
1610+
1611+
def test_stop_iteration_from_collect(pytester: Pytester) -> None:
1612+
pytester.makepyfile(test_it="raise StopIteration('hello')")
1613+
result = pytester.runpytest()
1614+
assert result.ret == ExitCode.INTERRUPTED
1615+
result.assert_outcomes(failed=0, passed=0, errors=1)
1616+
result.stdout.fnmatch_lines(
1617+
[
1618+
"=* short test summary info =*",
1619+
"ERROR test_it.py - StopIteration: hello",
1620+
"!* Interrupted: 1 error during collection !*",
1621+
"=* 1 error in * =*",
1622+
]
1623+
)
1624+
1625+
1626+
def test_stop_iteration_runtest_protocol(pytester: Pytester) -> None:
1627+
pytester.makepyfile(
1628+
test_it="""
1629+
import pytest
1630+
@pytest.fixture
1631+
def fail_setup():
1632+
raise StopIteration(1)
1633+
def test_fail_setup(fail_setup):
1634+
pass
1635+
def test_fail_teardown(request):
1636+
def stop_iteration():
1637+
raise StopIteration(2)
1638+
request.addfinalizer(stop_iteration)
1639+
def test_fail_call():
1640+
raise StopIteration(3)
1641+
"""
1642+
)
1643+
result = pytester.runpytest()
1644+
assert result.ret == ExitCode.TESTS_FAILED
1645+
result.assert_outcomes(failed=1, passed=1, errors=2)
1646+
result.stdout.fnmatch_lines(
1647+
[
1648+
"=* short test summary info =*",
1649+
"FAILED test_it.py::test_fail_call - StopIteration: 3",
1650+
"ERROR test_it.py::test_fail_setup - StopIteration: 1",
1651+
"ERROR test_it.py::test_fail_teardown - StopIteration: 2",
1652+
"=* 1 failed, 1 passed, 2 errors in * =*",
1653+
]
1654+
)

0 commit comments

Comments
 (0)