diff --git a/changelog/6257.improvement.rst b/changelog/6257.improvement.rst new file mode 100644 index 000000000..a21558286 --- /dev/null +++ b/changelog/6257.improvement.rst @@ -0,0 +1 @@ +Handle `exit.Exception` raised in `notify_exception` (via `pytest_internalerror`), e.g. when quitting pdb from post mortem. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index b4261c188..fc92ec10f 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -212,11 +212,17 @@ def wrap_session(config, doit): config.hook.pytest_keyboard_interrupt(excinfo=excinfo) session.exitstatus = exitstatus except: # noqa - excinfo = _pytest._code.ExceptionInfo.from_current() - config.notify_exception(excinfo, config.option) session.exitstatus = ExitCode.INTERNAL_ERROR - if excinfo.errisinstance(SystemExit): - sys.stderr.write("mainloop: caught unexpected SystemExit!\n") + excinfo = _pytest._code.ExceptionInfo.from_current() + try: + config.notify_exception(excinfo, config.option) + except exit.Exception as exc: + if exc.returncode is not None: + session.exitstatus = exc.returncode + sys.stderr.write("{}: {}\n".format(type(exc).__name__, exc)) + else: + if excinfo.errisinstance(SystemExit): + sys.stderr.write("mainloop: caught unexpected SystemExit!\n") finally: excinfo = None # Explicitly break reference cycle. diff --git a/testing/test_main.py b/testing/test_main.py new file mode 100644 index 000000000..b47791b29 --- /dev/null +++ b/testing/test_main.py @@ -0,0 +1,52 @@ +import pytest +from _pytest.main import ExitCode + + +@pytest.mark.parametrize( + "ret_exc", + ( + pytest.param((None, ValueError)), + pytest.param((42, SystemExit)), + pytest.param((False, SystemExit)), + ), +) +def test_wrap_session_notify_exception(ret_exc, testdir): + returncode, exc = ret_exc + c1 = testdir.makeconftest( + """ + import pytest + + def pytest_sessionstart(): + raise {exc}("boom") + + def pytest_internalerror(excrepr, excinfo): + returncode = {returncode!r} + if returncode is not False: + pytest.exit("exiting after %s..." % excinfo.typename, returncode={returncode!r}) + """.format( + returncode=returncode, exc=exc.__name__ + ) + ) + result = testdir.runpytest() + if returncode: + assert result.ret == returncode + else: + assert result.ret == ExitCode.INTERNAL_ERROR + assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):" + + if exc == SystemExit: + assert result.stdout.lines[-3:] == [ + 'INTERNALERROR> File "{}", line 4, in pytest_sessionstart'.format(c1), + 'INTERNALERROR> raise SystemExit("boom")', + "INTERNALERROR> SystemExit: boom", + ] + else: + assert result.stdout.lines[-3:] == [ + 'INTERNALERROR> File "{}", line 4, in pytest_sessionstart'.format(c1), + 'INTERNALERROR> raise ValueError("boom")', + "INTERNALERROR> ValueError: boom", + ] + if returncode is False: + assert result.stderr.lines == ["mainloop: caught unexpected SystemExit!"] + else: + assert result.stderr.lines == ["Exit: exiting after {}...".format(exc.__name__)]