Jump through excruciable hoops to ensure step-into and step-over commands are repeatable
authorMark Florisson <markflorisson88@gmail.com>
Sun, 7 Nov 2010 15:29:30 +0000 (16:29 +0100)
committerMark Florisson <markflorisson88@gmail.com>
Sun, 7 Nov 2010 15:29:30 +0000 (16:29 +0100)
(i.e. repeat commands when hitting enter on empty lines)

Cython/Debugger/Tests/test_libcython_in_gdb.py
Cython/Debugger/libcython.py
Cython/Debugger/libpython.py

index c33739147b58d0ff04a363646864f8abb8cfcce9..734df95fdcf046b0c4840acc87476a87328c060e 100644 (file)
@@ -69,7 +69,10 @@ class DebugTestCase(unittest.TestCase):
             gdb.execute('kill inferior 1', to_string=True)
         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):
index 5c7c891289fb493869d617e962ce001bccb0bd6e..ecee05ad31eb8ba1f0a978c8db18a793b0b3fc23 100644 (file)
@@ -687,7 +687,9 @@ class CyImport(CythonCommand):
                     cython_module.lineno_cy2c[cython_lineno] = min(c_linenos)
                     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:
index f7ac2d687c27169fb5e9faf216f413c4a88b079f..82631716b03bd291df02d2cc006952842db82a20 100644 (file)
@@ -50,6 +50,7 @@ import re
 import sys
 import atexit
 import tempfile
+import itertools
 
 import gdb
 
@@ -1593,30 +1594,95 @@ 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, 
                                                  gdb.COMMAND_RUNNING,
                                                  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):
         """
@@ -1624,13 +1690,7 @@ class GenericCodeStepper(gdb.Command):
         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:
-                result = gdb.execute('finish', to_string=True)
+                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,9 +1744,8 @@ 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
         
     def stopped(self):
@@ -1701,7 +1773,11 @@ class GenericCodeStepper(gdb.Command):
                 output = self.get_source_line(frame)
             
             if output is None:
-                print result.strip()
+                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