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): ...@@ -70,6 +70,9 @@ class DebugTestCase(unittest.TestCase):
except RuntimeError: except RuntimeError:
pass pass
libcython.cy.step.static_breakpoints.clear()
libcython.cy.step.runtime_breakpoints.clear()
libcython.cy.step.init_breakpoints()
class TestDebugInformationClasses(DebugTestCase): class TestDebugInformationClasses(DebugTestCase):
...@@ -123,19 +126,20 @@ class TestParameters(unittest.TestCase): ...@@ -123,19 +126,20 @@ class TestParameters(unittest.TestCase):
class TestBreak(DebugTestCase): class TestBreak(DebugTestCase):
def test_break(self): def test_break(self):
breakpoint_amount = len(gdb.breakpoints())
gdb.execute('cy break codefile.spam') gdb.execute('cy break codefile.spam')
self.assertEqual(len(gdb.breakpoints()), 1) self.assertEqual(len(gdb.breakpoints()), breakpoint_amount + 1)
bp, = gdb.breakpoints() bp = gdb.breakpoints()[-1]
self.assertEqual(bp.type, gdb.BP_BREAKPOINT) self.assertEqual(bp.type, gdb.BP_BREAKPOINT)
assert self.spam_func.cname in bp.location assert self.spam_func.cname in bp.location
assert bp.enabled assert bp.enabled
def test_python_break(self): def test_python_break(self):
gdb.execute('cy break -p join') gdb.execute('cy break -p join')
assert 'def join(' in gdb.execute('cy run', to_string=True) assert 'def join(' in gdb.execute('cy run', to_string=True)
class DebugStepperTestCase(DebugTestCase): class DebugStepperTestCase(DebugTestCase):
def step(self, varnames_and_values, source_line=None, lineno=None): def step(self, varnames_and_values, source_line=None, lineno=None):
......
...@@ -688,6 +688,8 @@ class CyImport(CythonCommand): ...@@ -688,6 +688,8 @@ class CyImport(CythonCommand):
for c_lineno in c_linenos: for c_lineno in c_linenos:
cython_module.lineno_c2cy[c_lineno] = cython_lineno cython_module.lineno_c2cy[c_lineno] = cython_lineno
self.cy.step.init_breakpoints()
class CyBreak(CythonCommand): class CyBreak(CythonCommand):
""" """
...@@ -839,11 +841,12 @@ class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper): ...@@ -839,11 +841,12 @@ class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper):
def register(cls): def register(cls):
return cls(cls.name, stepinto=getattr(cls, 'stepinto', False)) return cls(cls.name, stepinto=getattr(cls, 'stepinto', False))
def break_functions(self): def runtime_break_functions(self):
result = ['PyEval_EvalFrameEx']
if self.is_cython_function(): 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) result.extend(self.cy.functions_by_cname)
return result return result
...@@ -852,7 +855,7 @@ class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper): ...@@ -852,7 +855,7 @@ class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper):
if self.stepinto: if self.stepinto:
command = 'step' command = 'step'
else: else:
comamnd = 'next' command = 'next'
self.finish_executing(gdb.execute(command, to_string=True)) self.finish_executing(gdb.execute(command, to_string=True))
else: else:
......
...@@ -50,6 +50,7 @@ import re ...@@ -50,6 +50,7 @@ import re
import sys import sys
import atexit import atexit
import tempfile import tempfile
import itertools
import gdb import gdb
...@@ -1593,24 +1594,33 @@ class GenericCodeStepper(gdb.Command): ...@@ -1593,24 +1594,33 @@ class GenericCodeStepper(gdb.Command):
Superclass for code stepping. Subclasses must implement the following Superclass for code stepping. Subclasses must implement the following
methods: methods:
lineno(frame) - tells the current line number (only lineno(frame)
called for a relevant 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 is_relevant_function(frame)
current line (only called for a relevant tells whether we care about frame 'frame'
frame). If the source code cannot be
retrieved this function should get_source_line(frame)
return None get the line of source code for the current line (only called for a
break_functions() - an iterable of function names that are relevant frame). If the source code cannot be retrieved this
considered relevant and should halt function should return None
step-into execution. This is needed to
provide a performing step-into 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' 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 stepper = False
static_breakpoints = {}
runtime_breakpoints = {}
def __init__(self, name, stepinto=False): def __init__(self, name, stepinto=False):
super(GenericCodeStepper, self).__init__(name, super(GenericCodeStepper, self).__init__(name,
...@@ -1618,19 +1628,69 @@ class GenericCodeStepper(gdb.Command): ...@@ -1618,19 +1628,69 @@ class GenericCodeStepper(gdb.Command):
gdb.COMPLETE_NONE) gdb.COMPLETE_NONE)
self.stepinto = stepinto 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): def _step(self):
""" """
Do a single step or step-over. Returns the result of the last gdb Do a single step or step-over. Returns the result of the last gdb
command that made execution stop. command that made execution stop.
""" """
if self.stepinto: if self.stepinto:
# set breakpoints for any function we may end up in that seems self.enable_breakpoints()
# 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)
beginframe = gdb.selected_frame() beginframe = gdb.selected_frame()
beginline = self.lineno(beginframe) beginline = self.lineno(beginframe)
...@@ -1641,19 +1701,32 @@ class GenericCodeStepper(gdb.Command): ...@@ -1641,19 +1701,32 @@ class GenericCodeStepper(gdb.Command):
result = '' result = ''
while True: while True:
if self.stopped():
break
if self.is_relevant_function(newframe): if self.is_relevant_function(newframe):
result = gdb.execute('next', to_string=True) result = gdb.execute('next', to_string=True)
else:
if self._stackdepth(newframe) == 1:
result = gdb.execute('cont', to_string=True)
else: else:
result = gdb.execute('finish', to_string=True) result = gdb.execute('finish', to_string=True)
if result.startswith('Breakpoint'): if self.stopped():
break break
newframe = gdb.selected_frame() newframe = gdb.selected_frame()
is_relevant_function = self.is_relevant_function(newframe) 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: if newframe != beginframe:
# new function # new function
...@@ -1671,8 +1744,7 @@ class GenericCodeStepper(gdb.Command): ...@@ -1671,8 +1744,7 @@ class GenericCodeStepper(gdb.Command):
break break
if self.stepinto: if self.stepinto:
for bp in breakpoints: self.disable_breakpoints()
gdb.execute('delete %s' % bp, to_string=True)
return result return result
...@@ -1701,6 +1773,10 @@ class GenericCodeStepper(gdb.Command): ...@@ -1701,6 +1773,10 @@ class GenericCodeStepper(gdb.Command):
output = self.get_source_line(frame) output = self.get_source_line(frame)
if output is None: if output is None:
pframe = getattr(self, 'print_stackframe', None)
if pframe:
pframe(frame, index=0)
else:
print result.strip() print result.strip()
else: else:
print output print output
...@@ -1729,7 +1805,7 @@ class PythonCodeStepper(GenericCodeStepper): ...@@ -1729,7 +1805,7 @@ class PythonCodeStepper(GenericCodeStepper):
except IOError, e: except IOError, e:
return None return None
def break_functions(self): def static_break_functions(self):
yield 'PyEval_EvalFrameEx' yield 'PyEval_EvalFrameEx'
...@@ -1741,3 +1817,5 @@ class PyNext(PythonCodeStepper): ...@@ -1741,3 +1817,5 @@ class PyNext(PythonCodeStepper):
py_step = PyStep('py-step', stepinto=True) py_step = PyStep('py-step', stepinto=True)
py_next = PyNext('py-next', stepinto=False) 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