Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests for debugging imports #878

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
47 changes: 17 additions & 30 deletions ipykernel/debugger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pathlib import Path
import sys
import os
import re
import threading

import zmq
from zmq.utils import jsonapi
Expand Down Expand Up @@ -349,7 +349,7 @@ def _accept_stopped_thread(self, thread_name):
'Thread-4'
]
return thread_name not in forbid_list

async def handle_stopped_event(self):
# Wait for a stopped event message in the stopped queue
# This message is used for triggering the 'threads' request
Expand All @@ -375,8 +375,14 @@ def start(self):
if not os.path.exists(tmp_dir):
os.makedirs(tmp_dir)
host, port = self.debugpy_client.get_host_port()
code = 'import debugpy;'
code += 'debugpy.listen(("' + host + '",' + port + '))'
code = "import debugpy\n"
# Write debugpy logs?
#code += f'import debugpy; debugpy.log_to({str(Path(__file__).parent)!r});'
code += 'debugpy.listen(("' + host + '",' + port + '))\n'
code += (Path(__file__).parent / "filtered_pydb.py").read_text("utf8")
# Write pydevd logs?
# code += f'\npydevd.DebugInfoHolder.PYDEVD_DEBUG_FILE = {str(Path(__file__).parent / "debugpy.pydev.log")!r}\n'
# code += "pydevd.DebugInfoHolder.DEBUG_TRACE_LEVEL = 2\n"
content = {
'code': code,
'silent': True
Expand Down Expand Up @@ -449,29 +455,7 @@ async def source(self, message):
return reply

async def stackTrace(self, message):
reply = await self._forward_message(message)
# The stackFrames array can have the following content:
# { frames from the notebook}
# ...
# { 'id': xxx, 'name': '<module>', ... } <= this is the first frame of the code from the notebook
# { frames from ipykernel }
# ...
# {'id': yyy, 'name': '<module>', ... } <= this is the first frame of ipykernel code
# or only the frames from the notebook.
# We want to remove all the frames from ipykernel when they are present.
try:
sf_list = reply["body"]["stackFrames"]
module_idx = len(sf_list) - next(
i
for i, v in enumerate(reversed(sf_list), 1)
if v["name"] == "<module>" and i != 1
)
reply["body"]["stackFrames"] = reply["body"]["stackFrames"][
: module_idx + 1
]
except StopIteration:
pass
return reply
return await self._forward_message(message)

def accept_variable(self, variable_name):
forbid_list = [
Expand Down Expand Up @@ -524,8 +508,11 @@ async def attach(self, message):
# The ipykernel source is in the call stack, so the user
# has to manipulate the step-over and step-into in a wize way.
# Set debugOptions for breakpoints in python standard library source.
if not self.just_my_code:
message['arguments']['debugOptions'] = [ 'DebugStdLib' ]
message['arguments']['options'] = f'DEBUG_STDLIB={not self.just_my_code}'
# Explicitly ignore IPython implicit hooks ?
message['arguments']['rules'] = [
# { "module": "IPython.core.displayhook", "include": False },
]
return await self._forward_message(message)

async def configurationDone(self, message):
Expand Down Expand Up @@ -567,7 +554,7 @@ async def debugInfo(self, message):
async def inspectVariables(self, message):
self.variable_explorer.untrack_all()
# looks like the implementation of untrack_all in ptvsd
# destroys objects we nee din track. We have no choice but
# destroys objects we need in track. We have no choice but
# reinstantiate the object
self.variable_explorer = VariableExplorer()
self.variable_explorer.track()
Expand Down
76 changes: 76 additions & 0 deletions ipykernel/filtered_pydb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

import pydevd

__db = pydevd.get_global_debugger()
if __db:
__original = __db.get_file_type
__initial = True
def get_file_type(self, frame, abs_real_path_and_basename=None, _cache_file_type=pydevd._CACHE_FILE_TYPE):
'''
:param abs_real_path_and_basename:
The result from get_abs_path_real_path_and_base_from_file or
get_abs_path_real_path_and_base_from_frame.

:return
_pydevd_bundle.pydevd_dont_trace_files.PYDEV_FILE:
If it's a file internal to the debugger which shouldn't be
traced nor shown to the user.

_pydevd_bundle.pydevd_dont_trace_files.LIB_FILE:
If it's a file in a library which shouldn't be traced.

None:
If it's a regular user file which should be traced.
'''
global __initial
if __initial:
__initial = False
_cache_file_type.clear()
# Copied normalization:
if abs_real_path_and_basename is None:
try:
# Make fast path faster!
abs_real_path_and_basename = pydevd.NORM_PATHS_AND_BASE_CONTAINER[frame.f_code.co_filename]
except:
abs_real_path_and_basename = pydevd.get_abs_path_real_path_and_base_from_frame(frame)

cache_key = (frame.f_code.co_firstlineno, abs_real_path_and_basename[0], frame.f_code)
try:
return _cache_file_type[cache_key]
except KeyError:
pass

ret = __original(frame, abs_real_path_and_basename, _cache_file_type)
if ret is self.PYDEV_FILE:
return ret
if not hasattr(frame, "f_locals"):
return ret

# if either user or lib, check with our logic
# (we check "user" code in case any of the libs we use are in edit install)
# logic outline:
# - check if current frame is IPython bottom frame (if so filter it)
# - if not, check all ancestor for ipython bottom. Filter if not present.
# - if debugging / developing, do some sanity check of ignored frames, and log any unexecpted frames

# do not cache, these frames might show up on different sides of the bottom frame!
del _cache_file_type[cache_key]
if frame.f_locals.get("__tracebackhide__") == "__ipython_bottom__":
# Current frame is bottom frame, hide it!
pydevd.pydev_log.debug("Ignoring IPython bottom frame: %s - %s", frame.f_code.co_filename, frame.f_code.co_name)
ret = _cache_file_type[cache_key] = self.PYDEV_FILE
else:
f = frame
while f is not None:
if f.f_locals.get("__tracebackhide__") == "__ipython_bottom__":
# we found ipython bottom in stack, do not change type
return ret
f = f.f_back
pydevd.pydev_log.debug("Ignoring ipykernel frame: %s - %s", frame.f_code.co_filename, frame.f_code.co_name)

ret = self.PYDEV_FILE
return ret
__db.get_file_type = get_file_type.__get__(__db, pydevd.PyDB)
__db.is_files_filter_enabled = True
2 changes: 1 addition & 1 deletion ipykernel/kernelbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def _default_ident(self):
# Experimental option to break in non-user code.
# The ipykernel source is in the call stack, so the user
# has to manipulate the step-over and step-into in a wize way.
debug_just_my_code = Bool(True,
debug_just_my_code = Bool(os.environ.get("IPYKERNEL_DEBUG_JUST_MY_CODE", "True").lower() == "true",
help="""Set to False if you want to debug python standard and dependent libraries.
"""
).tag(config=True)
Expand Down
161 changes: 142 additions & 19 deletions ipykernel/tests/test_debugger.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from queue import Empty
import sys
import pytest

Expand Down Expand Up @@ -31,9 +32,32 @@ def wait_for_debug_request(kernel, command, arguments=None, full_reply=False):
return reply if full_reply else reply["content"]


def wait_for_debug_event(kernel, event, timeout=TIMEOUT, verbose=False, full_reply=False):
msg = {"msg_type": "", "content": {}}
while msg.get('msg_type') != 'debug_event' or msg["content"].get("event") != event:
msg = kernel.get_iopub_msg(timeout=timeout)
if verbose:
print(msg.get("msg_type"))
if (msg.get("msg_type") == "debug_event"):
print(f' {msg["content"].get("event")}')
return msg if full_reply else msg["content"]


def assert_stack_names(kernel, expected_names, thread_id=1):
reply = wait_for_debug_request(kernel, "stackTrace", {"threadId": thread_id})
names = [f.get("name") for f in reply["body"]["stackFrames"]]
# "<cell line: 1>" will be the name of the cell
assert names == expected_names


@pytest.fixture
def kernel():
with new_kernel() as kc:
def kernel(request):
if sys.platform == "win32":
import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
argv = getattr(request, "param", [])
#argv.append("--log-level=DEBUG")
with new_kernel(argv) as kc:
yield kc


Expand Down Expand Up @@ -144,7 +168,7 @@ def test_stop_on_breakpoint(kernel_with_debug):
kernel_with_debug,
"setBreakpoints",
{
"breakpoints": [{"line": 2}],
"breakpoints": [{"line": 2}, {"line": 5}],
"source": {"path": source},
"sourceModified": False,
},
Expand All @@ -153,16 +177,27 @@ def test_stop_on_breakpoint(kernel_with_debug):
wait_for_debug_request(kernel_with_debug, "configurationDone", full_reply=True)

kernel_with_debug.execute(code)

# Wait for stop on breakpoint
msg = {"msg_type": "", "content": {}}
while msg.get('msg_type') != 'debug_event' or msg["content"].get("event") != "stopped":
msg = kernel_with_debug.get_iopub_msg(timeout=TIMEOUT)
msg = wait_for_debug_event(kernel_with_debug, "stopped")
assert msg["body"]["reason"] == "breakpoint"
assert_stack_names(kernel_with_debug, ["<cell line: 5>"], r["body"].get("threadId", 1))

assert msg["content"]["body"]["reason"] == "breakpoint"
wait_for_debug_request(kernel_with_debug, "continue", {"threadId": msg["body"].get("threadId", 1)})

# Wait for stop on breakpoint
msg = wait_for_debug_event(kernel_with_debug, "stopped")
assert msg["body"]["reason"] == "breakpoint"
stacks = wait_for_debug_request(
kernel_with_debug,
"stackTrace",
{"threadId": r["body"].get("threadId", 1)}
)["body"]["stackFrames"]
names = [f.get("name") for f in stacks]
assert stacks[0]["line"] == 2
assert names == ["f", "<cell line: 5>"]


@pytest.mark.skipif(sys.version_info >= (3, 10), reason="TODO Does not work on Python 3.10")
def test_breakpoint_in_cell_with_leading_empty_lines(kernel_with_debug):
code = """
def f(a, b):
Expand All @@ -189,13 +224,10 @@ def f(a, b):
wait_for_debug_request(kernel_with_debug, "configurationDone", full_reply=True)

kernel_with_debug.execute(code)

# Wait for stop on breakpoint
msg = {"msg_type": "", "content": {}}
while msg.get('msg_type') != 'debug_event' or msg["content"].get("event") != "stopped":
msg = kernel_with_debug.get_iopub_msg(timeout=TIMEOUT)

assert msg["content"]["body"]["reason"] == "breakpoint"
# Wait for stop on breakpoint
msg = wait_for_debug_event(kernel_with_debug, "stopped")
assert msg["body"]["reason"] == "breakpoint"


def test_rich_inspect_not_at_breakpoint(kernel_with_debug):
Expand All @@ -220,6 +252,99 @@ def test_rich_inspect_not_at_breakpoint(kernel_with_debug):
assert reply["body"]["data"] == {"text/plain": f"'{value}'"}


@pytest.mark.parametrize("kernel", [["--Kernel.debug_just_my_code=False"]], indirect=True)
def test_step_into_lib(kernel_with_debug):
code = """import traitlets
traitlets.validate('foo', 'bar')
"""

r = wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
source = r["body"]["sourcePath"]

wait_for_debug_request(
kernel_with_debug,
"setBreakpoints",
{
"breakpoints": [{"line": 1}],
"source": {"path": source},
"sourceModified": False,
},
)

wait_for_debug_request(kernel_with_debug, "debugInfo")

wait_for_debug_request(kernel_with_debug, "configurationDone")
kernel_with_debug.execute(code)

# Wait for stop on breakpoint
r = wait_for_debug_event(kernel_with_debug, "stopped")
assert r["body"]["reason"] == "breakpoint"
assert_stack_names(kernel_with_debug, ["<cell line: 1>"], r["body"].get("threadId", 1))

# Step over the import statement
wait_for_debug_request(kernel_with_debug, "next", {"threadId": r["body"].get("threadId", 1)})
r = wait_for_debug_event(kernel_with_debug, "stopped")
assert r["body"]["reason"] == "step"
assert_stack_names(kernel_with_debug, ["<cell line: 2>"], r["body"].get("threadId", 1))

# Attempt to step into the function call
wait_for_debug_request(kernel_with_debug, "stepIn", {"threadId": r["body"].get("threadId", 1)})
r = wait_for_debug_event(kernel_with_debug, "stopped")
assert r["body"]["reason"] == "step"
assert_stack_names(kernel_with_debug, ["validate", "<cell line: 2>"], r["body"].get("threadId", 1))


# Test with both lib code and only "my code"
@pytest.mark.parametrize("kernel", [[], ["--Kernel.debug_just_my_code=False"]], indirect=True)
def test_step_into_end(kernel_with_debug):
code = 'a = 5 + 5'

r = wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
source = r["body"]["sourcePath"]

wait_for_debug_request(
kernel_with_debug,
"setBreakpoints",
{
"breakpoints": [{"line": 1}],
"source": {"path": source},
"sourceModified": False,
},
)

wait_for_debug_request(kernel_with_debug, "debugInfo")

wait_for_debug_request(kernel_with_debug, "configurationDone")
kernel_with_debug.execute(code)

# Wait for stop on breakpoint
r = wait_for_debug_event(kernel_with_debug, "stopped")

# Attempt to step into the statement (will continue execution, but
# should stop on first line of next execute request)
wait_for_debug_request(kernel_with_debug, "stepIn", {"threadId": r["body"].get("threadId", 1)})
# assert no stop statement is given
try:
r = wait_for_debug_event(kernel_with_debug, "stopped", timeout=3)
except Empty:
pass
else:
# we're stopped somewhere. Fail with trace
reply = wait_for_debug_request(kernel_with_debug, "stackTrace", {"threadId": r["body"].get("threadId", 1)})
entries = []
for f in reversed(reply["body"]["stackFrames"]):
source = f.get("source", {}).get("path") or "<unknown>"
loc = f'{source} ({f.get("line")},{f.get("column")})'
entries.append(f'{loc}: {f.get("name")}')
raise AssertionError('Unexpectedly stopped. Debugger stack:\n {0}'.format("\n ".join(entries)))

# execute some new code without breakpoints, assert it stops
code = 'print("bar")\nprint("alice")\n'
wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
kernel_with_debug.execute(code)
wait_for_debug_event(kernel_with_debug, "stopped")


def test_rich_inspect_at_breakpoint(kernel_with_debug):
code = """def f(a, b):
c = a + b
Expand Down Expand Up @@ -248,11 +373,9 @@ def test_rich_inspect_at_breakpoint(kernel_with_debug):
kernel_with_debug.execute(code)

# Wait for stop on breakpoint
msg = {"msg_type": "", "content": {}}
while msg.get('msg_type') != 'debug_event' or msg["content"].get("event") != "stopped":
msg = kernel_with_debug.get_iopub_msg(timeout=TIMEOUT)
r = wait_for_debug_event(kernel_with_debug, "stopped")

stacks = wait_for_debug_request(kernel_with_debug, "stackTrace", {"threadId": 1})[
stacks = wait_for_debug_request(kernel_with_debug, "stackTrace", {"threadId": r["body"].get("threadId", 1)})[
"body"
]["stackFrames"]

Expand Down