debugpy/tests/debugpy/test_exception.py
Rich Chiodo 4d86a42380
Updates from pydevd (#1706)
* Changes from pushing to pydevd

* Update some time outs to get flakey tests to pass

* Fix string failure

* String case backwards. Fixup test in pydevd

* Using callstack for exception check not sufficient

* Too restrictive on pydevd matching

* Try somethign better than just checking 'pydev'

* Retry a flakey test

* Disable flakey tests

* Another flakey test

* Increase timeout for attach

* Try upping timeout

* Up watchdog timeout

* Up some more timeouts

* Try delaying shutdown of test apps

* Don't output extra things that tests don't expect

* Fix output differences in 3.9? Not sure what that's about

* Fixup line differences in 3.9 with extra sleep

* Fix linter errors

* Fix breakpoint bugs
2024-10-21 10:17:39 -07:00

392 lines
14 KiB
Python

# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
import pytest
import sys
from _pydevd_bundle.pydevd_constants import IS_PY312_OR_GREATER
from tests import debug
from tests.debug import runners, targets
from tests.patterns import some
from tests.timeline import Event
str_matching_ArithmeticError = some.str.matching(r"(.+\.)?ArithmeticError")
@pytest.mark.parametrize("raised", ["raised", ""])
@pytest.mark.parametrize("uncaught", ["uncaught", ""])
def test_vsc_exception_options_raise_with_except(pyfile, target, run, raised, uncaught):
@pyfile
def code_to_debug():
import debuggee
debuggee.setup()
def raise_with_except():
try:
raise ArithmeticError("bad code") # @exc
except Exception:
pass
raise_with_except()
with debug.Session() as session:
session.expected_exit_code = some.int
with run(session, target(code_to_debug)):
session.request(
"setExceptionBreakpoints", {"filters": list({raised, uncaught} - {""})}
)
expected = some.dict.containing(
{
"exceptionId": str_matching_ArithmeticError,
"description": "bad code",
"breakMode": "always" if raised else "unhandled",
"details": some.dict.containing(
{
"typeName": str_matching_ArithmeticError,
"message": "bad code",
"source": some.path(code_to_debug),
}
),
}
)
if raised:
stop = session.wait_for_stop(
"exception",
expected_text=str_matching_ArithmeticError,
expected_description="bad code",
expected_frames=[some.dap.frame(code_to_debug, line="exc")],
)
exc_info = session.request("exceptionInfo", {"threadId": stop.thread_id})
assert exc_info == expected
session.request_continue()
if uncaught:
# Exception is caught by try..except, so there should be no stop.
pass
@pytest.mark.parametrize("raised", ["raised", ""])
@pytest.mark.parametrize("uncaught", ["uncaught", ""])
def test_vsc_exception_options_raise_without_except(
pyfile, target, run, raised, uncaught
):
@pyfile
def code_to_debug():
import debuggee
debuggee.setup()
def raise_without_except():
raise ArithmeticError("bad code") # @exc
raise_without_except()
with debug.Session() as session:
session.ignore_unobserved.append(Event("stopped"))
session.expected_exit_code = some.int
with run(session, target(code_to_debug)):
session.request(
"setExceptionBreakpoints", {"filters": list({raised, uncaught} - {""})}
)
expected_exc_info = some.dict.containing(
{
"exceptionId": str_matching_ArithmeticError,
"description": "bad code",
"breakMode": "always" if raised else "unhandled",
"details": some.dict.containing(
{
"typeName": str_matching_ArithmeticError,
"message": "bad code",
"source": some.path(code_to_debug),
}
),
}
)
if raised:
stop = session.wait_for_stop(
"exception", expected_frames=[some.dap.frame(code_to_debug, line="exc")]
)
exc_info = session.request("exceptionInfo", {"threadId": stop.thread_id})
assert expected_exc_info == exc_info
session.request_continue()
# NOTE: debugger stops at each frame if raised and is uncaught
# This behavior can be changed by updating 'notify_on_handled_exceptions'
# setting we send to pydevd to notify only once. In our test code, we have
# two frames, hence two stops.
session.wait_for_stop("exception")
session.request_continue()
if uncaught:
stop = session.wait_for_stop(
"exception", expected_frames=[some.dap.frame(code_to_debug, line="exc")]
)
expected_exc_info = some.dict.containing(
{
"exceptionId": str_matching_ArithmeticError,
"description": "bad code",
"breakMode": "unhandled", # Only difference from previous expected is breakMode.
"details": some.dict.containing(
{
"typeName": str_matching_ArithmeticError,
"message": "bad code",
"source": some.path(code_to_debug),
}
),
}
)
exc_info = session.request("exceptionInfo", {"threadId": stop.thread_id})
assert expected_exc_info == exc_info
session.request_continue()
@pytest.mark.skipif(
sys.platform == "darwin",
reason="https://github.com/microsoft/ptvsd/issues/1988",
)
@pytest.mark.flaky(retries=2, delay=1)
@pytest.mark.parametrize("target", targets.all_named)
@pytest.mark.parametrize("run", runners.all)
@pytest.mark.parametrize("raised", ["raised", ""])
@pytest.mark.parametrize("uncaught", ["uncaught", ""])
@pytest.mark.parametrize("zero", ["zero", ""])
@pytest.mark.parametrize("exit_code", [0, 1, "nan"])
def test_systemexit(pyfile, target, run, raised, uncaught, zero, exit_code):
@pyfile
def code_to_debug():
import debuggee
import sys
debuggee.setup()
exit_code = eval(sys.argv[1])
print("sys.exit(%r)" % (exit_code,))
try:
sys.exit(exit_code) # @handled
except SystemExit:
pass
sys.exit(exit_code) # @unhandled
filters = []
if raised:
filters += ["raised"]
if uncaught:
filters += ["uncaught"]
with debug.Session() as session:
session.expected_exit_code = some.int
session.config["breakOnSystemExitZero"] = bool(zero)
with run(session, target(code_to_debug, args=[repr(exit_code)])):
session.request("setExceptionBreakpoints", {"filters": filters})
# When breaking on raised exceptions, we'll stop on both lines,
# unless it's SystemExit(0) and we asked to ignore that.
if raised and (zero or exit_code != 0):
session.wait_for_stop(
"exception",
expected_frames=[some.dap.frame(code_to_debug, line="handled")],
)
session.request_continue()
session.wait_for_stop(
"exception",
expected_frames=[some.dap.frame(code_to_debug, line="unhandled")],
)
session.request_continue()
# When breaking on uncaught exceptions, we'll stop on the second line,
# unless it's SystemExit(0) and we asked to ignore that.
# Note that if both raised and uncaught filters are set, there will be
# two stop for the second line - one for exception being raised, and one
# for it unwinding the stack without finding a handler. The block above
# takes care of the first stop, so here we just take care of the second.
if uncaught and (zero or exit_code != 0):
session.wait_for_stop(
"exception",
expected_frames=[some.dap.frame(code_to_debug, line="unhandled")],
)
session.request_continue()
@pytest.mark.parametrize(
"break_mode", ["always", "never", "unhandled", "userUnhandled"]
)
@pytest.mark.parametrize(
"exceptions",
[
["RuntimeError"],
["AssertionError"],
["RuntimeError", "AssertionError"],
[], # Add the whole Python Exceptions category.
],
)
def test_raise_exception_options(pyfile, target, run, exceptions, break_mode):
if break_mode in ("never", "unhandled", "userUnhandled"):
expect_exceptions = []
if break_mode != "never" and (not exceptions or "AssertionError" in exceptions):
# Only AssertionError is raised in this use-case.
expect_exceptions = ["AssertionError"]
@pyfile
def code_to_debug():
import debuggee
debuggee.setup()
raise AssertionError() # @AssertionError
else:
expect_exceptions = exceptions[:]
if not expect_exceptions:
# Deal with the Python Exceptions category
expect_exceptions = ["RuntimeError", "AssertionError", "IndexError"]
@pyfile
def code_to_debug():
import debuggee
debuggee.setup()
try:
raise RuntimeError() # @RuntimeError
except RuntimeError:
pass
try:
raise AssertionError() # @AssertionError
except AssertionError:
pass
try:
raise IndexError() # @IndexError
except IndexError:
pass
with debug.Session() as session:
session.ignore_unobserved.append(Event("stopped"))
session.expected_exit_code = some.int
with run(session, target(code_to_debug)):
path = [{"names": ["Python Exceptions"]}]
if exceptions:
path.append({"names": exceptions})
session.request(
"setExceptionBreakpoints",
{
"filters": [], # Unused when exceptionOptions is passed.
"exceptionOptions": [
{
"path": path,
"breakMode": break_mode, # Can be "never", "always", "unhandled", "userUnhandled"
}
],
},
)
for expected_exception in expect_exceptions:
session.wait_for_stop(
"exception",
expected_frames=[
some.dap.frame(code_to_debug, line=expected_exception)
],
)
session.request_continue()
@pytest.mark.parametrize("target", targets.all_named)
@pytest.mark.parametrize("run", runners.all)
@pytest.mark.parametrize("exit_code", [0, 3])
@pytest.mark.parametrize("break_on_system_exit_zero", ["break_on_system_exit_zero", ""])
@pytest.mark.parametrize("django", ["django", ""])
@pytest.mark.skipif(sys.platform == 'win32' and IS_PY312_OR_GREATER, reason="Flakey test")
def test_success_exitcodes(
pyfile, target, run, exit_code, break_on_system_exit_zero, django
):
@pyfile
def code_to_debug():
import debuggee
import sys
debuggee.setup()
exit_code = eval(sys.argv[1])
print("sys.exit(%r)" % (exit_code,))
sys.exit(exit_code)
with debug.Session() as session:
session.expected_exit_code = some.int
session.config["breakOnSystemExitZero"] = bool(break_on_system_exit_zero)
session.config["django"] = bool(django)
with run(session, target(code_to_debug, args=[repr(exit_code)])):
session.request("setExceptionBreakpoints", {"filters": ["uncaught"]})
if break_on_system_exit_zero or (not django and exit_code == 3):
# If "breakOnSystemExitZero" was specified, we should always break.
# Otherwise, we should not break if the exit code indicates successful
# exit. 0 always indicates success, and 3 indicates failure only if
# Django debugging wasn't enabled.
session.wait_for_stop("exception")
session.request_continue()
@pytest.mark.parametrize("max_frames", ["default", "all", 10])
def test_exception_stack(pyfile, target, run, max_frames):
@pyfile
def code_to_debug():
import debuggee
debuggee.setup()
def do_something(n):
if n <= 0:
raise ArithmeticError("bad code") # @unhandled
do_something2(n - 1)
def do_something2(n):
do_something(n - 1)
do_something(100)
with debug.Session() as session:
session.expected_exit_code = some.int
max_frames, (min_expected_lines, max_expected_lines) = {
"all": (0, (100, 308)),
"default": (None, (100, 308)),
10: (10, (10, 32)),
}[max_frames]
if max_frames is not None:
session.config["maxExceptionStackFrames"] = max_frames
with run(session, target(code_to_debug)):
session.request("setExceptionBreakpoints", {"filters": ["uncaught"]})
stop = session.wait_for_stop(
"exception",
expected_frames=[some.dap.frame(code_to_debug, line="unhandled")],
)
exc_info = session.request("exceptionInfo", {"threadId": stop.thread_id})
expected_exc_info = some.dict.containing(
{
"exceptionId": str_matching_ArithmeticError,
"description": "bad code",
"breakMode": "unhandled",
"details": some.dict.containing(
{
"typeName": str_matching_ArithmeticError,
"message": "bad code",
"source": some.path(code_to_debug),
}
),
}
)
assert expected_exc_info == exc_info
stack_str = exc_info["details"]["stackTrace"]
stack_line_count = len(stack_str.split("\n"))
assert min_expected_lines <= stack_line_count <= max_expected_lines
session.request_continue()