Skip to content

gh-79871: IDLE - Fix and test debugger module #11451

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

Merged
merged 51 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
bbb58e8
start setting up tests for the Idb module
tonybaloney Jan 6, 2019
525efbc
handle NoneType in frame.f_back (first frame in session) and call bdb…
tonybaloney Jan 6, 2019
83aeefa
add a test for Idb to ensure calling super. Add a test to check the h…
tonybaloney Jan 6, 2019
d01dcdb
add test for user_exception to validate passing of exc_info to gui in…
tonybaloney Jan 6, 2019
6a123ef
add tests for in_rpc_code to catch various frame file names
tonybaloney Jan 6, 2019
e5e882c
add some PEP8 compliant line breaks
tonybaloney Jan 6, 2019
35915da
setup tests for the debugger.Debugger class
tonybaloney Jan 7, 2019
dbb5c7e
add tests for run with and without an idb instance set on init
tonybaloney Jan 7, 2019
2929019
add tests for interaction, which are currently demonstrating a bug in…
tonybaloney Jan 7, 2019
bbe4300
Bdb expects an instance attribute called 'botframe', which is set by …
tonybaloney Jan 7, 2019
4957496
remove empty single line comments
tonybaloney Jan 7, 2019
fbf1cac
add tests for the basic operations
tonybaloney Jan 7, 2019
2b42337
finished tests for Debugger
tonybaloney Jan 7, 2019
56741f1
add a constructor for MockFrameType to condense the code
tonybaloney Jan 7, 2019
8d015ce
add tests for show_source and load_stack for the StackViewer class
tonybaloney Jan 7, 2019
4807556
remove line comment
tonybaloney Jan 7, 2019
88e5f91
📜🤖 Added by blurb_it.
blurb-it[bot] Jan 7, 2019
0df2512
fixed references to .assertEquals, moved code into setup and teardown…
tonybaloney Jan 10, 2019
dfe6db9
cleanupm stackviewer tests and use Tk instance
tonybaloney Jan 10, 2019
2b9a43a
untoggle the gui requirement
tonybaloney Jan 10, 2019
5ae4060
Change the single-line docstrings to comments
tonybaloney Apr 4, 2019
14b908f
Removed the 2 interaction tests because they cause the UI to wait for…
tonybaloney Apr 5, 2019
ae491f4
Fix the test assertions and use stacked frames when testing debugging…
tonybaloney Apr 5, 2019
bc28ced
Make the test vaguer so that if the coverage checks pass it doesn't i…
tonybaloney Apr 5, 2019
9333fcf
Clean up attributes in memory and split tests between those which nee…
tonybaloney Apr 5, 2019
046fe00
Merge branch 'main' into idlelib_tests
terryjreedy Nov 14, 2023
daa2015
Update Misc/NEWS.d/next/IDLE/2019-01-07-06-18-25.bpo-35668.JimxP5.rst
terryjreedy Nov 14, 2023
37afb53
Update 2019-01-07-06-18-25.bpo-35668.JimxP5.rst
terryjreedy Nov 14, 2023
f4c3c00
Merge remote-tracking branch 'upstream/main' into pr_11451
terryjreedy Nov 15, 2023
e9d0e4e
Add comment to stackviewer.
terryjreedy Nov 15, 2023
3b97b55
Add debugger module docstrings and revise those for Idb.
terryjreedy Nov 15, 2023
d989bfa
Fix test failures by using 'assert_called_once_with'.
terryjreedy Nov 15, 2023
22e8249
Remove duplicate test.
terryjreedy Nov 15, 2023
2611447
Merge remote-tracking branch 'upstream/main' into pr_11451
terryjreedy Nov 15, 2023
f9f1a22
Remove added 'botframe' initialization as very likely not needed.
terryjreedy Nov 16, 2023
a202f4f
Move function frame2message to module level.
terryjreedy Nov 16, 2023
597cbba
IDLE style is no newline to start docstring.
terryjreedy Nov 16, 2023
0a53cd1
Change 'doc string' to 'Doc string.'
terryjreedy Nov 16, 2023
114d686
Remove unneeded Ibd setups and teardowns.
terryjreedy Nov 16, 2023
1c1107d
Polish IdbTest.
terryjreedy Nov 16, 2023
ed58072
Make in_rpc_code a module function, like frame2message.
terryjreedy Nov 16, 2023
d541302
Add FunctionTest
terryjreedy Nov 16, 2023
df1936f
Refactor IdbTest.
terryjreedy Nov 16, 2023
fedfbc0
FunctionTest comment
terryjreedy Nov 16, 2023
a731e63
Use False, True for Debugger().interacting.
terryjreedy Nov 16, 2023
9eb6548
Modify Debugger tests
terryjreedy Nov 16, 2023
0454ea5
Import Mock and patch, refactor to run many tests without gui.
terryjreedy Nov 16, 2023
63008a5
Put no-gui tests first, fix breakpoints, work on gui tests.
terryjreedy Nov 19, 2023
fe14bb8
Tests pass.
terryjreedy Nov 19, 2023
fd7e259
Merge branch 'main' into idlelib_tests
terryjreedy Nov 19, 2023
5fe9868
ws
terryjreedy Nov 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 44 additions & 11 deletions Lib/idlelib/debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@


class Idb(bdb.Bdb):
"""Idle debugger, based on the bdb debugger."""

def __init__(self, gui):
self.gui = gui # An instance of Debugger or proxy of remote.
bdb.Bdb.__init__(self)
self.botframe = None
super(Idb, self).__init__()

def user_line(self, frame):
"""
Handle a user stopping or breaking at a line.

Implements Bdb.user_line() to convert frame to string
and send message to GUI.
"""
if self.in_rpc_code(frame):
self.set_step()
return
Expand All @@ -25,18 +33,22 @@ def user_line(self, frame):
except TclError: # When closing debugger window with [x] in 3.x
pass

def user_exception(self, frame, info):
def user_exception(self, frame, exc_info):
"""Handle an the occurrence of an exception."""
if self.in_rpc_code(frame):
self.set_step()
return
message = self.__frame2message(frame)
self.gui.interaction(message, frame, info)
self.gui.interaction(message, frame, exc_info)

def in_rpc_code(self, frame):
"""Determine if debugger is within RPC code."""
if frame.f_code.co_filename.count('rpc.py'):
return True
else:
prev_frame = frame.f_back
if prev_frame is None:
return False
prev_name = prev_frame.f_code.co_filename
if 'idlelib' in prev_name and 'debugger' in prev_name:
# catch both idlelib/debugger.py and idlelib/debugger_r.py
Expand All @@ -45,6 +57,7 @@ def in_rpc_code(self, frame):
return self.in_rpc_code(prev_frame)

def __frame2message(self, frame):
"""Convert a frame to a message string."""
code = frame.f_code
filename = code.co_filename
lineno = frame.f_lineno
Expand All @@ -56,10 +69,30 @@ def __frame2message(self, frame):


class Debugger:

vstack = vsource = vlocals = vglobals = None
"""
The debugger interface.

This class handles the drawing of the debugger window and
the interactions with the underlying debugger session.
"""
vstack = None
vsource = None
vlocals = None
vglobals = None
stackviewer = None
localsviewer = None
globalsviewer = None

def __init__(self, pyshell, idb=None):
"""
Instantiate and draw a debugger window.

:param pyshell: An instance of the PyShell Window
:type pyshell: :class:`idlelib.pyshell.PyShell`

:param idb: An instance of the IDLE debugger (optional)
:type idb: :class:`idlelib.debugger.Idb`
"""
if idb is None:
idb = Idb(self)
self.pyshell = pyshell
Expand All @@ -70,6 +103,7 @@ def __init__(self, pyshell, idb=None):
self.nesting_level = 0

def run(self, *args):
"""Run the debugger."""
# Deal with the scenario where we've already got a program running
# in the debugger and we want to start another. If that is the case,
# our second 'run' was invoked from an event dispatched not from
Expand Down Expand Up @@ -110,6 +144,7 @@ def run(self, *args):
self.interacting = 0

def close(self, event=None):
"""Close the debugger and window."""
try:
self.quit()
except Exception:
Expand All @@ -127,6 +162,7 @@ def close(self, event=None):
self.top.destroy()

def make_gui(self):
"""Draw the debugger gui on the screen."""
pyshell = self.pyshell
self.flist = pyshell.flist
self.root = root = pyshell.root
Expand Down Expand Up @@ -287,8 +323,6 @@ def quit(self):
def abort_loop(self):
self.root.tk.call('set', '::idledebugwait', '1')

stackviewer = None

def show_stack(self):
if not self.stackviewer and self.vstack.get():
self.stackviewer = sv = StackViewer(self.fstack, self.flist, self)
Expand All @@ -310,9 +344,6 @@ def show_frame(self, stackitem):
self.frame = stackitem[0] # lineno is stackitem[1]
self.show_variables()

localsviewer = None
globalsviewer = None

def show_locals(self):
lv = self.localsviewer
if self.vlocals.get():
Expand Down Expand Up @@ -363,7 +394,7 @@ def clear_file_breaks(self, filename):
self.idb.clear_all_file_breaks(filename)

def load_breakpoints(self):
"Load PyShellEditorWindow breakpoints into subprocess debugger"
"""Load PyShellEditorWindow breakpoints into subprocess debugger."""
for editwin in self.pyshell.flist.inversedict:
filename = editwin.io.filename
try:
Expand All @@ -372,6 +403,7 @@ def load_breakpoints(self):
except AttributeError:
continue


class StackViewer(ScrolledList):

def __init__(self, master, flist, gui):
Expand Down Expand Up @@ -543,6 +575,7 @@ def load_dict(self, dict, force=0, rpc_client=None):
def close(self):
self.frame.destroy()


if __name__ == "__main__":
from unittest import main
main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)
Expand Down
217 changes: 215 additions & 2 deletions Lib/idlelib/idle_test/test_debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from idlelib import debugger
import unittest
from unittest import mock
from test.support import requires
requires('gui')
# requires('gui')
from tkinter import Tk
from textwrap import dedent


class NameSpaceTest(unittest.TestCase):
Expand All @@ -23,7 +25,218 @@ def test_init(self):
debugger.NamespaceViewer(self.root, 'Test')


# Other classes are Idb, Debugger, and StackViewer.
class MockFrameType(object):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDLE tests aren't backported to 2.7, so you don't need to include object when defining a class.

"""Reflection of the types.FrameType."""
f_back = None
f_builtins = None
f_code = None
f_globals = None
f_lasti = None
f_lineno = None
f_locals = None
f_restricted = 0
f_trace = None

def __init__(self):
pass


class IdbTest(unittest.TestCase):

def test_init_runs_bdb_init(self):
"""Test that Idb calls the base Bdb __init__."""
idb = debugger.Idb(None)
assert idb.gui is None
assert hasattr(idb, 'breaks')

def test_user_line_basic_frame(self):
"""Test that .user_line() creates a string message for a frame."""

# Create a test code object to simulate a debug session.
test_code = dedent("""
i = 1
i += 2
if i == 3:
print(i)
""")
code_obj = compile(test_code,
'test_user_line_basic_frame.py',
mode='exec')

# Create 2 test frames for lines 1 and 2 of the test code.
test_frame1 = MockFrameType()
test_frame1.f_code = code_obj
test_frame1.f_lineno = 1

test_frame2 = MockFrameType()
test_frame2.f_code = code_obj
test_frame2.f_lineno = 2
test_frame2.f_back = test_frame1

gui = mock.Mock()
gui.interaction = mock.Mock()

idb = debugger.Idb(gui)
idb.user_line(test_frame2)

assert not idb.in_rpc_code(test_frame2)
gui.interaction.assert_called_once()
gui.interaction.assert_called_with('test_user_line_basic_frame.py:2: <module>()', test_frame2)

def test_user_exception(self):
"""Test that .user_exception() creates a string message for a frame."""

# Create a test code object to simulate a debug session.
test_code = dedent("""
i = 1
i += 2
if i == 3:
print(i)
""")
code_obj = compile(test_code,
'test_user_exception.py',
mode='exec')

# Create 1 test frame
test_frame1 = MockFrameType()
test_frame1.f_code = code_obj
test_frame1.f_lineno = 1

# Example from sys.exc_info()
test_exc_info = (type(ValueError), ValueError(), None)

gui = mock.Mock()
gui.interaction = mock.Mock()

idb = debugger.Idb(gui)
idb.user_exception(test_frame1, test_exc_info)

assert not idb.in_rpc_code(test_frame1)
gui.interaction.assert_called_once()
gui.interaction.assert_called_with('test_user_exception.py:1: <module>()', test_frame1, test_exc_info)

def test_in_rpc_code_rpc_py(self):
"""Test that .in_rpc_code detects position of rpc.py."""

# Create a test code object to simulate a debug session.
test_code = dedent("""
i = 1
i += 2
if i == 3:
print(i)
""")
code_obj = compile(test_code,
'rpc.py',
mode='exec')

# Create 1 test frame
test_frame = MockFrameType()
test_frame.f_code = code_obj
test_frame.f_lineno = 1

gui = mock.Mock()
gui.interaction = mock.Mock()

idb = debugger.Idb(gui)

assert idb.in_rpc_code(test_frame)

def test_in_rpc_code_debugger_star_dot_py(self):
"""Test that .in_rpc_code detects position of idlelib/debugger*.py."""

# Create a test code object to simulate a debug session.
for filename in ('idlelib/debugger.py', 'idlelib/debugger_r.py'):
test_code = dedent("""
i = 1
i += 2
if i == 3:
print(i)
""")
code_obj = compile(test_code,
filename,
mode='exec')

# Create 2 test frames
test_frame = MockFrameType()
test_frame.f_code = code_obj
test_frame.f_lineno = 1

test_frame2 = MockFrameType()
test_frame2.f_code = code_obj
test_frame2.f_lineno = 2
test_frame2.f_back = test_frame

gui = mock.Mock()
gui.interaction = mock.Mock()

idb = debugger.Idb(gui)

assert not idb.in_rpc_code(test_frame2)


def make_pyshell_mock():
"""Factory for generating test fixtures of PyShell."""
pyshell = mock.Mock()
pyshell.root = None
return pyshell


class DebuggerTest(unittest.TestCase):
"""Tests for the idlelib.debugger.Debugger class."""

def test_setup_debugger(self):
"""Test that Debugger can be instantiated with a mock PyShell."""
pyshell = make_pyshell_mock()
test_debugger = debugger.Debugger(pyshell)

assert test_debugger.pyshell == pyshell
assert test_debugger.frame is None

def test_run_debugger_with_idb(self):
"""Test Debugger.run() with an Idb instance."""
mock_idb = mock.Mock() # Mocked debugger.Idb
test_debugger = debugger.Debugger(make_pyshell_mock(), idb=mock_idb)
test_debugger.run(1, 'two')
mock_idb.run.assert_called_once()
mock_idb.run.called_with(1, 'two')
assert test_debugger.interacting == 0

def test_run_debugger_no_idb(self):
"""Test Debugger.run() with no Idb instance."""
test_debugger = debugger.Debugger(make_pyshell_mock(), idb=None)
assert test_debugger.idb is not None
test_debugger.idb.run = mock.Mock()
test_debugger.run(1, 'two')
test_debugger.idb.run.assert_called_once()
test_debugger.idb.run.called_with(1, 'two')
assert test_debugger.interacting == 0

def test_close(self):
"""Test closing the window in an idle state."""
pyshell = make_pyshell_mock()
test_debugger = debugger.Debugger(pyshell)
test_debugger.close()
pyshell.close_debugger.assert_called_once()

def test_close_whilst_interacting(self):
"""Test closing the window in an interactive state."""
pyshell = make_pyshell_mock()
test_debugger = debugger.Debugger(pyshell)
test_debugger.interacting = 1
test_debugger.close()
pyshell.close_debugger.assert_not_called()

def test_interaction_with_message_and_frame(self):
test_message = "testing 1234.."
test_frame = MockFrameType()

pyshell = make_pyshell_mock()
test_debugger = debugger.Debugger(pyshell)

# Patch out the status label so we can check messages
test_debugger.status = mock.Mock()
test_debugger.interaction(test_message, test_frame)


if __name__ == '__main__':
unittest.main(verbosity=2)