cy backtrace
authorMark Florisson <markflorisson88@gmail.com>
Tue, 2 Nov 2010 21:15:32 +0000 (22:15 +0100)
committerMark Florisson <markflorisson88@gmail.com>
Tue, 2 Nov 2010 21:15:32 +0000 (22:15 +0100)
Cython/Debugger/Tests/test_libcython_in_gdb.py
Cython/Debugger/libcython.py
Cython/Debugger/libpython.py

index 060d64c154999103170c9010d07e0cc133ab2458..f3126d9f020c6a8f5fac3da344c4a813a7a19b98 100644 (file)
@@ -6,6 +6,7 @@ Cython.Debugger.Cygdb.make_command_file()
 """
 
 import os
+import re
 import sys
 import trace
 import inspect
@@ -42,7 +43,7 @@ class DebugTestCase(unittest.TestCase):
             'codefile.eggs']
     
     def read_var(self, varname, cast_to=None):
-        result = gdb.parse_and_eval('$cy_cname("%s")' % varname)
+        result = gdb.parse_and_eval('$cy_cvalue("%s")' % varname)
         if cast_to:
             result = cast_to(result)
         
@@ -113,6 +114,7 @@ class TestDebugInformationClasses(DebugTestCase):
 class TestParameters(unittest.TestCase):
     
     def test_parameters(self):
+        gdb.execute('set cy_colorize_code on')
         assert libcython.parameters.colorize_code
         gdb.execute('set cy_colorize_code off')
         assert not libcython.parameters.colorize_code
@@ -121,20 +123,19 @@ class TestParameters(unittest.TestCase):
 class TestBreak(DebugTestCase):
 
     def test_break(self):
-        result = libpython._execute('cy break codefile.spam', to_string=True)
-        assert self.spam_func.cname in result
+        gdb.execute('cy break codefile.spam')
         
         self.assertEqual(len(gdb.breakpoints()), 1)
         bp, = gdb.breakpoints()
         self.assertEqual(bp.type, gdb.BP_BREAKPOINT)
-        self.assertEqual(bp.location, self.spam_func.cname)
+        assert self.spam_func.cname in bp.location
         assert bp.enabled
 
         
 class DebugStepperTestCase(DebugTestCase):
     
     def step(self, varnames_and_values, source_line=None, lineno=None):
-        gdb.execute(self.command, to_string=True)
+        gdb.execute(self.command)
         for varname, value in varnames_and_values:
             self.assertEqual(self.read_var(varname), value, self.local_info())
         
@@ -177,14 +178,14 @@ class TestStep(DebugStepperTestCase):
     def test_python_step(self):
         self.break_and_run('os.path.join("foo", "bar")')
         
-        gdb.execute('cy step', to_string=True)
+        result = gdb.execute('cy step', to_string=True)
         
         curframe = gdb.selected_frame()
         self.assertEqual(curframe.name(), 'PyEval_EvalFrameEx')
         
         pyframe = libpython.Frame(curframe).get_pyop()
         self.assertEqual(str(pyframe.co_name), 'join')
-
+        assert re.match(r'\d+    def join\(', result), result
 
 class TestNext(DebugStepperTestCase):
     
@@ -218,11 +219,64 @@ class TestLocalsGlobals(DebugTestCase):
         self.break_and_run('int(10)')
         
         result = gdb.execute('cy globals', to_string=True)
-        assert '__name__ =' in result, repr(result)
-        assert '__doc__ =' in result, repr(result)
-        assert 'os =' in result, repr(result)
-        assert 'c_var = 12' in result, repr(result)
-        assert 'python_var = 13' in result, repr(result)
+        assert '__name__ ' in result, repr(result)
+        assert '__doc__ ' in result, repr(result)
+        assert 'os ' in result, repr(result)
+        assert 'c_var ' in result, repr(result)
+        assert 'python_var ' in result, repr(result)
+
+
+class TestBacktrace(DebugTestCase):
+    
+    def test_backtrace(self):
+        libcython.parameters.colorize_code.value = False
+        
+        self.break_and_run('os.path.join("foo", "bar")')
+        result = gdb.execute('cy bt', to_string=True)
+        assert re.search(r'\#\d+ *0x.* in spam\(\) at .*codefile\.pyx:22', 
+                         result), result
+        assert 'os.path.join("foo", "bar")' in result, result
+        
+        gdb.execute("cy step")
+        
+        gdb.execute('cy bt')
+        result = gdb.execute('cy bt -a', to_string=True)
+        assert re.search(r'\#0 *0x.* in main\(\) at', result), result
+
+
+class TestFunctions(DebugTestCase):
+    
+    def test_functions(self):
+        self.break_and_run('c = 2')
+        result = gdb.execute('print $cy_cname("b")', to_string=True)
+        assert re.search('__pyx_.*b', result), result
+        
+        result = gdb.execute('print $cy_lineno()', to_string=True)
+        supposed_lineno = test_libcython.source_to_lineno['c = 2']
+        assert str(supposed_lineno) in result, (supposed_lineno, result)
+        
+        result = gdb.execute('print $cy_cvalue("b")', to_string=True)
+        assert '= 1' in result
+        
+
+class TestPrint(DebugTestCase):
+    
+    def test_print(self):
+        self.break_and_run('c = 2')
+        result = gdb.execute('cy print b', to_string=True)
+        assert '= 1' in result
+
+
+class TestUpDown(DebugTestCase):
+
+    def test_updown(self):
+        self.break_and_run('os.path.join("foo", "bar")')
+        gdb.execute('cy step')
+        self.assertRaises(RuntimeError, gdb.execute, 'cy down')
+        
+        result = gdb.execute('cy up', to_string=True)
+        assert 'spam()' in result
+        assert 'os.path.join("foo", "bar")' in result
 
 
 def _main():
index 059e7ef5ed73ff1d72d1e7fb7227f4ce007309f5..d6df99be84f4f6db4e244e6ba7d1e6e2f766a933 100644 (file)
@@ -2,6 +2,7 @@
 GDB extension that adds Cython support.
 """
 
+import os
 import sys
 import textwrap
 import traceback
@@ -69,7 +70,7 @@ def dont_suppress_errors(function):
 def default_selected_gdb_frame(err=True):
     def decorator(function):
         @functools.wraps(function)
-        def wrapper(self, frame=None, **kwargs):
+        def wrapper(self, frame=None, *args, **kwargs):
             try:
                 frame = frame or gdb.selected_frame()
             except RuntimeError:
@@ -78,14 +79,16 @@ def default_selected_gdb_frame(err=True):
             if err and frame.name() is None:
                 raise NoFunctionNameInFrameError()
     
-            return function(self, frame, **kwargs)
+            return function(self, frame, *args, **kwargs)
         return wrapper
     return decorator
 
 def require_cython_frame(function):
     @functools.wraps(function)
+    @require_running_program
     def wrapper(self, *args, **kwargs):
-        if not self.is_cython_function():
+        frame = kwargs.get('frame') or gdb.selected_frame()
+        if not self.is_cython_function(frame):
             raise gdb.GdbError('Selected frame does not correspond with a '
                                'Cython function we know about.')
         return function(self, *args, **kwargs)
@@ -111,6 +114,17 @@ def dispatch_on_frame(c_command, python_command=None):
         return wrapper
     return decorator
 
+def require_running_program(function):
+    @functools.wraps(function)
+    def wrapper(*args, **kwargs):
+        try:
+            gdb.selected_frame()
+        except RuntimeError:
+            raise gdb.GdbError("No frame is currently selected.")
+        
+        return function(*args, **kwargs)
+    return wrapper
+    
 
 # Classes that represent the debug information
 # Don't rename the parameters of these classes, they come directly from the XML
@@ -206,12 +220,12 @@ class CythonBase(object):
     @default_selected_gdb_frame()
     def get_source_desc(self, frame):
         filename = lineno = lexer = None
-        if self.is_cython_function():
+        if self.is_cython_function(frame):
             filename = self.get_cython_function(frame).module.filename
             lineno = self.get_cython_lineno(frame)
             if pygments:
                 lexer = pygments.lexers.CythonLexer()
-        elif self.is_python_function():
+        elif self.is_python_function(frame):
             pyframeobject = libpython.Frame(frame).get_pyop()
 
             if not pyframeobject:
@@ -221,7 +235,17 @@ class CythonBase(object):
             lineno = pyframeobject.current_line_num()
             if pygments:
                 lexer = pygments.lexers.PythonLexer()
-
+        else:
+            symbol_and_line_obj = frame.find_sal()
+            if symbol_and_line_obj is None:
+                filename = None
+                lineno = 0
+            else:
+                filename = symbol_and_line_obj.symtab.filename
+                lineno = symbol_and_line_obj.line
+                if pygments:
+                    lexer = pygments.lexers.CLexer()
+            
         return SourceFileDescriptor(filename, lexer), lineno
 
     @default_selected_gdb_frame()
@@ -255,6 +279,61 @@ class CythonBase(object):
             # variable not initialized yet
             pass
 
+    @default_selected_gdb_frame()
+    def print_stackframe(self, frame, index, is_c=False):
+        """
+        Print a C, Cython or Python stack frame and the line of source code
+        if available.
+        """
+        # do this to prevent the require_cython_frame decorator from
+        # raising GdbError when calling self.cy.cy_cvalue.invoke()
+        selected_frame = gdb.selected_frame()
+        frame.select()
+        
+        source_desc, lineno = self.get_source_desc(frame)
+        
+        if not is_c and self.is_python_function(frame):
+            pyframe = libpython.Frame(frame).get_pyop()
+            if pyframe is None or pyframe.is_optimized_out():
+                # print this python function as a C function
+                return self.print_stackframe(frame, index, is_c=True)
+            
+            func_name = pyframe.co_name
+            func_cname = 'PyEval_EvalFrameEx'
+            func_args = []
+        elif self.is_cython_function(frame):
+            cyfunc = self.get_cython_function(frame)
+            f = lambda arg: self.cy.cy_cvalue.invoke(arg, frame=frame)
+            
+            func_name = cyfunc.name
+            func_cname = cyfunc.cname
+            func_args = [] # [(arg, f(arg)) for arg in cyfunc.arguments]
+        else:
+            source_desc, lineno = self.get_source_desc(frame)
+            func_name = frame.name()
+            func_cname = func_name
+            func_args = []
+        
+        gdb_value = gdb.parse_and_eval(func_cname)
+        # Seriously? Why is the address not an int?
+        func_address = int(str(gdb_value.address).split()[0], 0)
+        
+        print '#%-2d 0x%016x in %s(%s) at %s:%s' % (
+            index,
+            func_address,
+            func_name,
+            ', '.join('%s=%s' % (name, val) for name, val in func_args),
+            source_desc.filename,
+            lineno)
+            
+        try:
+            print '    ' + source_desc.get_source(lineno)
+        except gdb.GdbError:
+            pass
+        
+        selected_frame.select()
+            
+
 class SourceFileDescriptor(object):
     def __init__(self, filename, lexer, formatter=None):
         self.filename = filename
@@ -413,14 +492,25 @@ class CythonCommand(gdb.Command, CythonBase):
     """
     Base class for Cython commands
     """
-
+    
+    command_class = gdb.COMMAND_NONE
+    completer_class = gdb.COMPLETE_NONE
+    
     @classmethod
-    def register(cls, *args, **kwargs):
+    def _register(cls, clsname, args, kwargs):
         if not hasattr(cls, 'completer_class'):
             return cls(cls.name, cls.command_class, *args, **kwargs)
         else:
             return cls(cls.name, cls.command_class, cls.completer_class, 
                        *args, **kwargs)
+    
+    @classmethod
+    def register(cls, *args, **kwargs):
+        alias = getattr(cls, 'alias', None)
+        if alias:
+            cls._register(cls.alias, args, kwargs)
+            
+        return cls._register(cls.name, args, kwargs)
 
 
 class CyCy(CythonCommand):
@@ -435,11 +525,11 @@ class CyCy(CythonCommand):
         cy cont
         cy up
         cy down
+        cy bt / cy backtrace
         cy print
         cy list
         cy locals
         cy globals
-        cy backtrace
     """
     
     name = 'cy'
@@ -458,12 +548,14 @@ class CyCy(CythonCommand):
             cont = CyCont.register(),
             up = CyUp.register(),
             down = CyDown.register(),
+            bt = CyBacktrace.register(),
             list = CyList.register(),
             print_ = CyPrint.register(),
             locals = CyLocals.register(),
             globals = CyGlobals.register(),
             cy_cname = CyCName('cy_cname'),
-            cy_line = CyLine('cy_line'),
+            cy_cvalue = CyCValue('cy_cvalue'),
+            cy_lineno = CyLine('cy_lineno'),
         )
             
         for command_name, command in commands.iteritems():
@@ -687,7 +779,7 @@ class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper):
     @classmethod
     def register(cls):
         return cls(cls.name, stepper=cls.stepper)
-    
+
 
 class CyStep(CythonCodeStepper):
     "Step through Python code."
@@ -743,10 +835,20 @@ class CyUp(CythonCodeStepper):
     _command = 'up'
     
     def invoke(self, *args):
-        self.result = gdb.execute(self._command, to_string=True)
-        while not self.is_relevant_function(gdb.selected_frame()):
-            self.result = gdb.execute(self._command, to_string=True)
-        self.end_stepping()
+        try:
+            gdb.execute(self._command, to_string=True)
+            while not self.is_relevant_function(gdb.selected_frame()):
+                gdb.execute(self._command, to_string=True)
+        except RuntimeError, e:
+            raise gdb.GdbError(*e.args)
+        
+        frame = gdb.selected_frame()
+        index = 0
+        while frame:
+            frame = frame.older()
+            index += 1
+            
+        self.print_stackframe(index=index - 1)
 
 
 class CyDown(CyUp):
@@ -758,6 +860,35 @@ class CyDown(CyUp):
     _command = 'down'
 
 
+class CyBacktrace(CythonCommand):
+    'Print the Cython stack'
+    
+    name = 'cy bt'
+    alias = 'cy backtrace'
+    command_class = gdb.COMMAND_STACK
+    completer_class = gdb.COMPLETE_NONE
+    
+    @require_running_program
+    def invoke(self, args, from_tty):
+        # get the first frame
+        selected_frame = frame = gdb.selected_frame()
+        while frame.older():
+            frame = frame.older()
+        
+        print_all = args == '-a'
+        
+        index = 0
+        while frame:
+            is_c = False
+            
+            if print_all or self.is_relevant_function(frame):
+                self.print_stackframe(frame, index)
+            
+            index += 1
+            frame = frame.newer()
+        
+        selected_frame.select()
+
 class CyList(CythonCommand):
     """
     List Cython source code. To disable to customize colouring see the cy_*
@@ -785,7 +916,7 @@ class CyPrint(CythonCommand):
     
     @dispatch_on_frame(c_command='print', python_command='py-print')
     def invoke(self, name, from_tty, max_name_length=None):
-        cname = self.cy.cy_cname.invoke(name, string=True)
+        cname = self.cy.cy_cname.invoke(name)
         try:
              value = gdb.parse_and_eval(cname)
         except RuntimeError, e:
@@ -880,7 +1011,7 @@ class CyCName(gdb.Function, CythonBase):
     """
     
     @require_cython_frame
-    def invoke(self, cyname, string=False, frame=None):
+    def invoke(self, cyname, frame=None):
         frame = frame or gdb.selected_frame()
         cname = None
         
@@ -905,12 +1036,20 @@ class CyCName(gdb.Function, CythonBase):
         if not cname:
             raise gdb.GdbError('No such Cython variable: %s' % cyname)
         
-        if string:
-            return cname
-        else:
-            return gdb.parse_and_eval(cname)
+        return cname
 
 
+class CyCValue(CyCName):
+    """
+    Get the value of a Cython variable.
+    """
+    
+    @require_cython_frame
+    def invoke(self, cyname, frame=None):
+        cname = super(CyCValue, self).invoke(cyname, frame=frame)
+        return gdb.parse_and_eval(cname)
+    
+
 class CyLine(gdb.Function, CythonBase):
     """
     Get the current Cython line.
index 12195438e2a3e5db60f9385610a2084ac40a7c2b..880105dc9cd75d4cb81f57016817dbf0f6462e4a 100644 (file)
@@ -1444,12 +1444,13 @@ class PyLocals(gdb.Command):
         namespace = self.get_namespace(pyop_frame)
         namespace = [(name.proxyval(set()), val) for name, val in namespace]
         
-        name, val = max(namespace, key=lambda (name, val): len(name))
-        max_name_length = len(name)
-        
-        for name, pyop_value in namespace:
-            value = pyop_value.get_truncated_repr(MAX_OUTPUT_LEN)
-            print ('%-*s = %s' % (max_name_length, name, value))
+        if namespace:
+            name, val = max(namespace, key=lambda (name, val): len(name))
+            max_name_length = len(name)
+            
+            for name, pyop_value in namespace:
+                value = pyop_value.get_truncated_repr(MAX_OUTPUT_LEN)
+                print ('%-*s = %s' % (max_name_length, name, value))
 
     def get_namespace(self, pyop_frame):
         return pyop_frame.iter_locals()