Skip to content

Commit a8fc056

Browse files
authored
Handle Exit exception in pytest_sessionfinish (#6660)
2 parents ef437ea + 99d162e commit a8fc056

File tree

3 files changed

+46
-10
lines changed

3 files changed

+46
-10
lines changed

changelog/6660.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:func:`pytest.exit() <_pytest.outcomes.exit>` is handled when emitted from the :func:`pytest_sessionfinish <_pytest.hookspec.pytest_sessionfinish>` hook. This includes quitting from a debugger.

src/_pytest/main.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import importlib
66
import os
77
import sys
8+
from typing import Callable
89
from typing import Dict
910
from typing import FrozenSet
1011
from typing import List
@@ -23,7 +24,7 @@
2324
from _pytest.config import UsageError
2425
from _pytest.fixtures import FixtureManager
2526
from _pytest.nodes import Node
26-
from _pytest.outcomes import exit
27+
from _pytest.outcomes import Exit
2728
from _pytest.runner import collect_one_node
2829
from _pytest.runner import SetupState
2930

@@ -194,7 +195,9 @@ def pytest_addoption(parser):
194195
)
195196

196197

197-
def wrap_session(config, doit):
198+
def wrap_session(
199+
config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
200+
) -> Union[int, ExitCode]:
198201
"""Skeleton command line program"""
199202
session = Session(config)
200203
session.exitstatus = ExitCode.OK
@@ -211,10 +214,10 @@ def wrap_session(config, doit):
211214
raise
212215
except Failed:
213216
session.exitstatus = ExitCode.TESTS_FAILED
214-
except (KeyboardInterrupt, exit.Exception):
217+
except (KeyboardInterrupt, Exit):
215218
excinfo = _pytest._code.ExceptionInfo.from_current()
216-
exitstatus = ExitCode.INTERRUPTED
217-
if isinstance(excinfo.value, exit.Exception):
219+
exitstatus = ExitCode.INTERRUPTED # type: Union[int, ExitCode]
220+
if isinstance(excinfo.value, Exit):
218221
if excinfo.value.returncode is not None:
219222
exitstatus = excinfo.value.returncode
220223
if initstate < 2:
@@ -228,7 +231,7 @@ def wrap_session(config, doit):
228231
excinfo = _pytest._code.ExceptionInfo.from_current()
229232
try:
230233
config.notify_exception(excinfo, config.option)
231-
except exit.Exception as exc:
234+
except Exit as exc:
232235
if exc.returncode is not None:
233236
session.exitstatus = exc.returncode
234237
sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc))
@@ -237,12 +240,18 @@ def wrap_session(config, doit):
237240
sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
238241

239242
finally:
240-
excinfo = None # Explicitly break reference cycle.
243+
# Explicitly break reference cycle.
244+
excinfo = None # type: ignore
241245
session.startdir.chdir()
242246
if initstate >= 2:
243-
config.hook.pytest_sessionfinish(
244-
session=session, exitstatus=session.exitstatus
245-
)
247+
try:
248+
config.hook.pytest_sessionfinish(
249+
session=session, exitstatus=session.exitstatus
250+
)
251+
except Exit as exc:
252+
if exc.returncode is not None:
253+
session.exitstatus = exc.returncode
254+
sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc))
246255
config._ensure_unconfigure()
247256
return session.exitstatus
248257

@@ -382,6 +391,7 @@ class Session(nodes.FSCollector):
382391
_setupstate = None # type: SetupState
383392
# Set on the session by fixtures.pytest_sessionstart.
384393
_fixturemanager = None # type: FixtureManager
394+
exitstatus = None # type: Union[int, ExitCode]
385395

386396
def __init__(self, config: Config) -> None:
387397
nodes.FSCollector.__init__(

testing/test_main.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from typing import Optional
2+
13
import pytest
24
from _pytest.main import ExitCode
5+
from _pytest.pytester import Testdir
36

47

58
@pytest.mark.parametrize(
@@ -50,3 +53,25 @@ def pytest_internalerror(excrepr, excinfo):
5053
assert result.stderr.lines == ["mainloop: caught unexpected SystemExit!"]
5154
else:
5255
assert result.stderr.lines == ["Exit: exiting after {}...".format(exc.__name__)]
56+
57+
58+
@pytest.mark.parametrize("returncode", (None, 42))
59+
def test_wrap_session_exit_sessionfinish(
60+
returncode: Optional[int], testdir: Testdir
61+
) -> None:
62+
testdir.makeconftest(
63+
"""
64+
import pytest
65+
def pytest_sessionfinish():
66+
pytest.exit(msg="exit_pytest_sessionfinish", returncode={returncode})
67+
""".format(
68+
returncode=returncode
69+
)
70+
)
71+
result = testdir.runpytest()
72+
if returncode:
73+
assert result.ret == returncode
74+
else:
75+
assert result.ret == ExitCode.NO_TESTS_COLLECTED
76+
assert result.stdout.lines[-1] == "collected 0 items"
77+
assert result.stderr.lines == ["Exit: exit_pytest_sessionfinish"]

0 commit comments

Comments
 (0)