Commit dabdcf42 authored by Mark Florisson's avatar Mark Florisson

Jump through excruciable hoops to ensure step-into and step-over commands are repeatable

(i.e. repeat commands when hitting enter on empty lines)
parent ebbf5499
......@@ -70,6 +70,9 @@ class DebugTestCase(unittest.TestCase):
except RuntimeError:
pass
libcython.cy.step.static_breakpoints.clear()
libcython.cy.step.runtime_breakpoints.clear()
libcython.cy.step.init_breakpoints()
class TestDebugInformationClasses(DebugTestCase):
......@@ -123,19 +126,20 @@ class TestParameters(unittest.TestCase):
class TestBreak(DebugTestCase):
def test_break(self):
breakpoint_amount = len(gdb.breakpoints())
gdb.execute('cy break codefile.spam')
self.assertEqual(len(gdb.breakpoints()), 1)
bp, = gdb.breakpoints()
self.assertEqual(len(gdb.breakpoints()), breakpoint_amount + 1)
bp = gdb.breakpoints()[-1]
self.assertEqual(bp.type, gdb.BP_BREAKPOINT)
assert self.spam_func.cname in bp.location
assert bp.enabled
def test_python_break(self):
gdb.execute('cy break -p join')
assert 'def join(' in gdb.execute('cy run', to_string=True)
class DebugStepperTestCase(DebugTestCase):
def step(self, varnames_and_values, source_line=None, lineno=None):
......
......@@ -688,6 +688,8 @@ class CyImport(CythonCommand):
for c_lineno in c_linenos:
cython_module.lineno_c2cy[c_lineno] = cython_lineno
self.cy.step.init_breakpoints()
class CyBreak(CythonCommand):
"""
......@@ -839,11 +841,12 @@ class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper):
def register(cls):
return cls(cls.name, stepinto=getattr(cls, 'stepinto', False))
def break_functions(self):
result = ['PyEval_EvalFrameEx']
def runtime_break_functions(self):
if self.is_cython_function():
result.extend(self.get_cython_function().step_into_functions)
return self.get_cython_function().step_into_functions
def static_break_functions(self):
result = ['PyEval_EvalFrameEx']
result.extend(self.cy.functions_by_cname)
return result
......@@ -852,7 +855,7 @@ class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper):
if self.stepinto:
command = 'step'
else:
comamnd = 'next'
command = 'next'
self.finish_executing(gdb.execute(command, to_string=True))
else:
......
......@@ -50,6 +50,7 @@ import re
import sys
import atexit
import tempfile
import itertools
import gdb
......@@ -1593,24 +1594,33 @@ class GenericCodeStepper(gdb.Command):
Superclass for code stepping. Subclasses must implement the following
methods:
lineno(frame) - tells the current line number (only
called for a relevant frame)
is_relevant_function(frame) - tells whether we care about frame 'frame'
get_source_line(frame) - get the line of source code for the
current line (only called for a relevant
frame). If the source code cannot be
retrieved this function should
return None
break_functions() - an iterable of function names that are
considered relevant and should halt
step-into execution. This is needed to
provide a performing step-into
lineno(frame)
tells the current line number (only called for a relevant frame)
is_relevant_function(frame)
tells whether we care about frame 'frame'
get_source_line(frame)
get the line of source code for the current line (only called for a
relevant frame). If the source code cannot be retrieved this
function should return None
static_break_functions()
returns an iterable of function names that are considered relevant
and should halt step-into execution. This is needed to provide a
performing step-into
runtime_break_functions
list of functions that we should break into depending on the
context
This class provides an 'invoke' method that invokes a 'step' or 'step-over'
depending on the 'stepper' argument.
depending on the 'stepinto' argument.
"""
stepper = False
static_breakpoints = {}
runtime_breakpoints = {}
def __init__(self, name, stepinto=False):
super(GenericCodeStepper, self).__init__(name,
......@@ -1618,19 +1628,69 @@ class GenericCodeStepper(gdb.Command):
gdb.COMPLETE_NONE)
self.stepinto = stepinto
def _break_func(self, funcname):
result = gdb.execute('break %s' % funcname, to_string=True)
return re.search(r'Breakpoint (\d+)', result).group(1)
def init_breakpoints(self):
"""
Keep all breakpoints around and simply disable/enable them each time
we are stepping. We need this because if you set and delete a
breakpoint, gdb will not repeat your command (this is due to 'delete').
Why? I'm buggered if I know. To further annoy us, we can't use the
breakpoint API because there's no option to make breakpoint setting
silent.
So now! We may have an insane amount of breakpoints to list when the
user does 'info breakpoints' :(
This method must be called whenever the list of functions we should
step into changes. It can be called on any GenericCodeStepper instance.
"""
break_funcs = set(self.static_break_functions())
for funcname in break_funcs:
if funcname not in self.static_breakpoints:
self.static_breakpoints[funcname] = self._break_func(funcname)
for bp in set(self.static_breakpoints) - break_funcs:
gdb.execute("delete " + self.static_breakpoints[bp])
self.disable_breakpoints()
def enable_breakpoints(self):
for bp in self.static_breakpoints.itervalues():
gdb.execute('enable ' + bp)
runtime_break_functions = self.runtime_break_functions()
if runtime_break_functions is None:
return
for funcname in runtime_break_functions:
if (funcname not in self.static_breakpoints and
funcname not in self.runtime_breakpoints):
self.runtime_breakpoints[funcname] = self._break_func(funcname)
elif funcname in self.runtime_breakpoints:
gdb.execute('enable ' + self.runtime_breakpoints[funcname])
def disable_breakpoints(self):
chain = itertools.chain(self.static_breakpoints.itervalues(),
self.runtime_breakpoints.itervalues())
for bp in chain:
gdb.execute('disable ' + bp)
def runtime_break_functions(self):
"""
Implement this if the list of step-into functions depends on the
context.
"""
def _step(self):
"""
Do a single step or step-over. Returns the result of the last gdb
command that made execution stop.
"""
if self.stepinto:
# set breakpoints for any function we may end up in that seems
# relevant
breakpoints = []
for break_func in self.break_functions():
result = gdb.execute('break %s' % break_func, to_string=True)
bp = re.search(r'Breakpoint (\d+)', result).group(1)
breakpoints.append(bp)
self.enable_breakpoints()
beginframe = gdb.selected_frame()
beginline = self.lineno(beginframe)
......@@ -1641,19 +1701,32 @@ class GenericCodeStepper(gdb.Command):
result = ''
while True:
if self.stopped():
break
if self.is_relevant_function(newframe):
result = gdb.execute('next', to_string=True)
else:
if self._stackdepth(newframe) == 1:
result = gdb.execute('cont', to_string=True)
else:
result = gdb.execute('finish', to_string=True)
if result.startswith('Breakpoint'):
if self.stopped():
break
newframe = gdb.selected_frame()
is_relevant_function = self.is_relevant_function(newframe)
try:
framename = newframe.name()
except RuntimeError:
framename = None
m = re.search(r'Breakpoint (\d+)', result)
if m:
bp = self.runtime_breakpoints.get(framename)
if bp is None or (m.group(1) == bp and is_relevant_function):
# although we hit a breakpoint, we still need to check
# that the function, in case hit by a runtime breakpoint,
# is in the right context
break
if newframe != beginframe:
# new function
......@@ -1671,8 +1744,7 @@ class GenericCodeStepper(gdb.Command):
break
if self.stepinto:
for bp in breakpoints:
gdb.execute('delete %s' % bp, to_string=True)
self.disable_breakpoints()
return result
......@@ -1701,6 +1773,10 @@ class GenericCodeStepper(gdb.Command):
output = self.get_source_line(frame)
if output is None:
pframe = getattr(self, 'print_stackframe', None)
if pframe:
pframe(frame, index=0)
else:
print result.strip()
else:
print output
......@@ -1729,7 +1805,7 @@ class PythonCodeStepper(GenericCodeStepper):
except IOError, e:
return None
def break_functions(self):
def static_break_functions(self):
yield 'PyEval_EvalFrameEx'
......@@ -1741,3 +1817,5 @@ class PyNext(PythonCodeStepper):
py_step = PyStep('py-step', stepinto=True)
py_next = PyNext('py-next', stepinto=False)
py_step.init_breakpoints()
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment