From 38f33b6e2227fe1d88f56a163890604858b6e1a2 Mon Sep 17 00:00:00 2001 From: Mark Florisson Date: Tue, 2 Nov 2010 22:15:32 +0100 Subject: [PATCH] cy backtrace --- .../Debugger/Tests/test_libcython_in_gdb.py | 78 ++++++-- Cython/Debugger/libcython.py | 181 ++++++++++++++++-- Cython/Debugger/libpython.py | 13 +- 3 files changed, 233 insertions(+), 39 deletions(-) diff --git a/Cython/Debugger/Tests/test_libcython_in_gdb.py b/Cython/Debugger/Tests/test_libcython_in_gdb.py index 060d64c1..f3126d9f 100644 --- a/Cython/Debugger/Tests/test_libcython_in_gdb.py +++ b/Cython/Debugger/Tests/test_libcython_in_gdb.py @@ -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(): diff --git a/Cython/Debugger/libcython.py b/Cython/Debugger/libcython.py index 059e7ef5..d6df99be 100644 --- a/Cython/Debugger/libcython.py +++ b/Cython/Debugger/libcython.py @@ -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. diff --git a/Cython/Debugger/libpython.py b/Cython/Debugger/libpython.py index 12195438..880105dc 100644 --- a/Cython/Debugger/libpython.py +++ b/Cython/Debugger/libpython.py @@ -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() -- 2.26.2