Handle `Exit` exception in `pytest_sessionfinish` (#6660)

This commit is contained in:
Daniel Hahler 2020-02-07 00:40:10 +01:00 committed by GitHub
commit a8fc056aad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 46 additions and 10 deletions

View File

@ -0,0 +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.

View File

@ -5,6 +5,7 @@ import functools
import importlib import importlib
import os import os
import sys import sys
from typing import Callable
from typing import Dict from typing import Dict
from typing import FrozenSet from typing import FrozenSet
from typing import List from typing import List
@ -23,7 +24,7 @@ from _pytest.config import hookimpl
from _pytest.config import UsageError from _pytest.config import UsageError
from _pytest.fixtures import FixtureManager from _pytest.fixtures import FixtureManager
from _pytest.nodes import Node from _pytest.nodes import Node
from _pytest.outcomes import exit from _pytest.outcomes import Exit
from _pytest.runner import collect_one_node from _pytest.runner import collect_one_node
from _pytest.runner import SetupState from _pytest.runner import SetupState
@ -194,7 +195,9 @@ def pytest_addoption(parser):
) )
def wrap_session(config, doit): def wrap_session(
config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]]
) -> Union[int, ExitCode]:
"""Skeleton command line program""" """Skeleton command line program"""
session = Session(config) session = Session(config)
session.exitstatus = ExitCode.OK session.exitstatus = ExitCode.OK
@ -211,10 +214,10 @@ def wrap_session(config, doit):
raise raise
except Failed: except Failed:
session.exitstatus = ExitCode.TESTS_FAILED session.exitstatus = ExitCode.TESTS_FAILED
except (KeyboardInterrupt, exit.Exception): except (KeyboardInterrupt, Exit):
excinfo = _pytest._code.ExceptionInfo.from_current() excinfo = _pytest._code.ExceptionInfo.from_current()
exitstatus = ExitCode.INTERRUPTED exitstatus = ExitCode.INTERRUPTED # type: Union[int, ExitCode]
if isinstance(excinfo.value, exit.Exception): if isinstance(excinfo.value, Exit):
if excinfo.value.returncode is not None: if excinfo.value.returncode is not None:
exitstatus = excinfo.value.returncode exitstatus = excinfo.value.returncode
if initstate < 2: if initstate < 2:
@ -228,7 +231,7 @@ def wrap_session(config, doit):
excinfo = _pytest._code.ExceptionInfo.from_current() excinfo = _pytest._code.ExceptionInfo.from_current()
try: try:
config.notify_exception(excinfo, config.option) config.notify_exception(excinfo, config.option)
except exit.Exception as exc: except Exit as exc:
if exc.returncode is not None: if exc.returncode is not None:
session.exitstatus = exc.returncode session.exitstatus = exc.returncode
sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc)) sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc))
@ -237,12 +240,18 @@ def wrap_session(config, doit):
sys.stderr.write("mainloop: caught unexpected SystemExit!\n") sys.stderr.write("mainloop: caught unexpected SystemExit!\n")
finally: finally:
excinfo = None # Explicitly break reference cycle. # Explicitly break reference cycle.
excinfo = None # type: ignore
session.startdir.chdir() session.startdir.chdir()
if initstate >= 2: if initstate >= 2:
try:
config.hook.pytest_sessionfinish( config.hook.pytest_sessionfinish(
session=session, exitstatus=session.exitstatus session=session, exitstatus=session.exitstatus
) )
except Exit as exc:
if exc.returncode is not None:
session.exitstatus = exc.returncode
sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc))
config._ensure_unconfigure() config._ensure_unconfigure()
return session.exitstatus return session.exitstatus
@ -382,6 +391,7 @@ class Session(nodes.FSCollector):
_setupstate = None # type: SetupState _setupstate = None # type: SetupState
# Set on the session by fixtures.pytest_sessionstart. # Set on the session by fixtures.pytest_sessionstart.
_fixturemanager = None # type: FixtureManager _fixturemanager = None # type: FixtureManager
exitstatus = None # type: Union[int, ExitCode]
def __init__(self, config: Config) -> None: def __init__(self, config: Config) -> None:
nodes.FSCollector.__init__( nodes.FSCollector.__init__(

View File

@ -1,5 +1,8 @@
from typing import Optional
import pytest import pytest
from _pytest.main import ExitCode from _pytest.main import ExitCode
from _pytest.pytester import Testdir
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -50,3 +53,25 @@ def test_wrap_session_notify_exception(ret_exc, testdir):
assert result.stderr.lines == ["mainloop: caught unexpected SystemExit!"] assert result.stderr.lines == ["mainloop: caught unexpected SystemExit!"]
else: else:
assert result.stderr.lines == ["Exit: exiting after {}...".format(exc.__name__)] assert result.stderr.lines == ["Exit: exiting after {}...".format(exc.__name__)]
@pytest.mark.parametrize("returncode", (None, 42))
def test_wrap_session_exit_sessionfinish(
returncode: Optional[int], testdir: Testdir
) -> None:
testdir.makeconftest(
"""
import pytest
def pytest_sessionfinish():
pytest.exit(msg="exit_pytest_sessionfinish", returncode={returncode})
""".format(
returncode=returncode
)
)
result = testdir.runpytest()
if returncode:
assert result.ret == returncode
else:
assert result.ret == ExitCode.NO_TESTS_COLLECTED
assert result.stdout.lines[-1] == "collected 0 items"
assert result.stderr.lines == ["Exit: exit_pytest_sessionfinish"]