diff options
-rw-r--r-- | libpython.py | 277 | ||||
-rw-r--r-- | test_gdb.py | 265 |
2 files changed, 456 insertions, 86 deletions
diff --git a/libpython.py b/libpython.py index 1ede863..e994af2 100644 --- a/libpython.py +++ b/libpython.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # -# Copyright © 2010 Red Hat, Inc. +# Copyright © 2010 David Hugh Malcolm # # This software is licensed to you under the GNU Lesser General Public # License, version 2.1 (LGPLv2.1). There is NO WARRANTY for this software, @@ -10,11 +10,7 @@ # LGPLv2.1 along with this software; if not, see # http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt # -# Red Hat trademarks are not licensed under LGPLv2.1. No permission is -# granted to use or replicate Red Hat trademarks that are incorporated in -# this software or its documentation. -# -# Red Hat Author(s): David Hugh Malcolm <dmalcolm@redhat.com> +# Author: Dave Malcolm <dmalcolm@redhat.com> ''' From gdb 7 onwards, gdb's build can be configured --with-python, allowing gdb to be extended with Python code e.g. for library-specific data visualizations, @@ -49,30 +45,43 @@ TODO: better handling of "instance" import gdb +# Look up the gdb.Type for some standard types: +_type_char_ptr = gdb.lookup_type('char').pointer() # char* +_type_void_ptr = gdb.lookup_type('void').pointer() # void* +_type_size_t = gdb.lookup_type('size_t') + +SIZEOF_VOID_P = _type_void_ptr.sizeof + + +Py_TPFLAGS_HEAPTYPE = (1L << 9) + +Py_TPFLAGS_INT_SUBCLASS = (1L << 23) +Py_TPFLAGS_LONG_SUBCLASS = (1L << 24) +Py_TPFLAGS_LIST_SUBCLASS = (1L << 25) +Py_TPFLAGS_TUPLE_SUBCLASS = (1L << 26) +Py_TPFLAGS_STRING_SUBCLASS = (1L << 27) +Py_TPFLAGS_UNICODE_SUBCLASS = (1L << 28) +Py_TPFLAGS_DICT_SUBCLASS = (1L << 29) +Py_TPFLAGS_BASE_EXC_SUBCLASS = (1L << 30) +Py_TPFLAGS_TYPE_SUBCLASS = (1L << 31) + + class NullPyObjectPtr(RuntimeError): pass + def safety_limit(val): # Given a integer value from the process being debugged, limit it to some # safety threshold so that arbitrary breakage within said process doesn't # break the gdb process too much (e.g. sizes of iterations, sizes of lists) return min(val, 100) + def safe_range(val): # As per range, but don't trust the value too much: cap it to a safety # threshold in case the data was corrupted return xrange(safety_limit(val)) -def is_py3k(): - # This code assumes that a libpython's DWARF data has actually been - # loaded by the point that this function is called - sym = gdb.lookup_symbol('PyBytes_Type')[0] - if sym: - #...then PyBytes_Type exists, assume this is libpython3.* - return True - else: - #...then PyBytes_Type doesn't exist, assume this is libpython2.* - return False class PyObjectPtr(object): """ @@ -87,7 +96,7 @@ class PyObjectPtr(object): """ _typename = 'PyObject' - def __init__(self, gdbval, cast_to = None): + def __init__(self, gdbval, cast_to=None): if cast_to: self._gdbval = gdbval.cast(cast_to) else: @@ -100,10 +109,10 @@ class PyObjectPtr(object): Various libpython types are defined using the "PyObject_HEAD" and "PyObject_VAR_HEAD" macros. - - In Python 2, this these are defined so that "ob_type" and (for a var + + In Python 2, this these are defined so that "ob_type" and (for a var object) "ob_size" are fields of the type in question. - + In Python 3, this is defined as an embedded PyVarObject type thus: PyVarObject ob_base; so that the "ob_size" field is located insize the "ob_base" field, and @@ -113,8 +122,9 @@ class PyObjectPtr(object): raise NullPyObjectPtr(self) if name == 'ob_type': - return self._gdbval.cast(PyObjectPtr.get_gdb_type()).dereference()[name] - + pyo_ptr = self._gdbval.cast(PyObjectPtr.get_gdb_type()) + return pyo_ptr.dereference()[name] + if name == 'ob_size': try: # Python 2: @@ -122,7 +132,7 @@ class PyObjectPtr(object): except RuntimeError: # Python 3: return self._gdbval.dereference()['ob_base'][name] - + # General case: look it up inside the object: return self._gdbval.dereference()[name] @@ -132,6 +142,16 @@ class PyObjectPtr(object): def is_null(self): return 0 == long(self._gdbval) + def safe_tp_name(self): + try: + return self.type().field('tp_name').string() + except NullPyObjectPtr: + # NULL tp_name? + return 'unknown' + except RuntimeError: + # Can't even read the object at all? + return 'unknown' + def proxyval(self): ''' Scrape a value from the inferior process, and try to represent it @@ -143,12 +163,14 @@ class PyObjectPtr(object): For example, a PyIntObject* with ob_ival 42 in the inferior process should result in an int(42) in this process. ''' + class FakeRepr(object): """ Class representing a non-descript PyObject* value in the inferior process for when we don't have a custom scraper, intended to have a sane repr(). """ + def __init__(self, tp_name, address): self.tp_name = tp_name self.address = address @@ -156,43 +178,70 @@ class PyObjectPtr(object): def __repr__(self): return '<%s at remote 0x%x>' % (self.tp_name, self.address) - try: - tp_name = self.type().field('tp_name').string() - except NullPyObjectPtr: - # NULL tp_name? - tp_name = 'unknown' - except RuntimeError: - # Can't even read the object at all? - tp_name = 'unknown' - return FakeRepr(tp_name, + return FakeRepr(self.safe_tp_name(), long(self._gdbval)) @classmethod - def subclass_for_tp_name(cls, tp_name): - if tp_name == 'str': - if is_py3k(): - return PyUnicodeObjectPtr - else: - return PyStringObjectPtr - if tp_name == 'int': - if is_py3k(): - return PyLongObjectPtr - else: - return PyIntObjectPtr - - name_map = {'bool' : PyBoolObjectPtr, + def subclass_from_type(cls, t): + ''' + Given a PyTypeObjectPtr instance wrapping a gdb.Value that's a + (PyTypeObject*), determine the corresponding subclass of PyObjectPtr + to use + + Ideally, we would look up the symbols for the global types, but that + isn't working yet: + (gdb) python print gdb.lookup_symbol('PyList_Type')[0].value + Traceback (most recent call last): + File "<string>", line 1, in <module> + NotImplementedError: Symbol type not yet supported in Python scripts. + Error while executing Python code. + + For now, we use tp_flags, after doing some string comparisons on the + tp_name for some special-cases that don't seem to be visible through + flags + ''' + try: + tp_name = t.field('tp_name').string() + tp_flags = int(t.field('tp_flags')) + except RuntimeError: + # Handle any kind of error e.g. NULL ptrs by simply using the base + # class + return cls + + #print 'tp_flags = 0x%08x' % tp_flags + #print 'tp_name = %r' % tp_name + + name_map = {'bool': PyBoolObjectPtr, 'classobj': PyClassObjectPtr, - 'dict': PyDictObjectPtr, 'instance': PyInstanceObjectPtr, - 'list': PyListObjectPtr, - 'long': PyLongObjectPtr, 'NoneType': PyNoneStructPtr, - 'tuple': PyTupleObjectPtr, 'frame': PyFrameObjectPtr, - 'unicode': PyUnicodeObjectPtr, } if tp_name in name_map: return name_map[tp_name] + + if tp_flags & Py_TPFLAGS_HEAPTYPE: + return HeapTypeObjectPtr + + if tp_flags & Py_TPFLAGS_INT_SUBCLASS: + return PyIntObjectPtr + if tp_flags & Py_TPFLAGS_LONG_SUBCLASS: + return PyLongObjectPtr + if tp_flags & Py_TPFLAGS_LIST_SUBCLASS: + return PyListObjectPtr + if tp_flags & Py_TPFLAGS_TUPLE_SUBCLASS: + return PyTupleObjectPtr + if tp_flags & Py_TPFLAGS_STRING_SUBCLASS: + return PyStringObjectPtr + if tp_flags & Py_TPFLAGS_UNICODE_SUBCLASS: + return PyUnicodeObjectPtr + if tp_flags & Py_TPFLAGS_DICT_SUBCLASS: + return PyDictObjectPtr + #if tp_flags & Py_TPFLAGS_BASE_EXC_SUBCLASS: + # return something + #if tp_flags & Py_TPFLAGS_TYPE_SUBCLASS: + # return PyTypeObjectPtr + # Use the base class: return cls @@ -200,26 +249,11 @@ class PyObjectPtr(object): def from_pyobject_ptr(cls, gdbval): ''' Try to locate the appropriate derived class dynamically, and cast - the pointer accordingly: - For now, we just do string comparison on the tp_name - Other approaches: - (i) look up the symbols for the global types, but that isn't working yet: - (gdb) python print gdb.lookup_symbol('PyList_Type')[0].value - Traceback (most recent call last): - File "<string>", line 1, in <module> - NotImplementedError: Symbol type not yet supported in Python scripts. - Error while executing Python code. - (ii) look at tp_flags, looking e.g. for Py_TPFLAGS_LIST_SUBCLASS however - this would rely on the values of those flags. - - So we go with the simple approach of looking at tp_name + the pointer accordingly. ''' - # try: p = PyObjectPtr(gdbval) - t = p.type() - tp_name = t.field('tp_name').string() - cls = cls.subclass_for_tp_name(tp_name) + cls = cls.subclass_from_type(p.type()) return cls(gdbval, cast_to=cls.get_gdb_type()) except RuntimeError: # Handle any kind of error e.g. NULL ptrs by simply using the base @@ -231,6 +265,68 @@ class PyObjectPtr(object): def get_gdb_type(cls): return gdb.lookup_type(cls._typename).pointer() + +class InstanceProxy(object): + + def __init__(self, cl_name, attrdict, address): + self.cl_name = cl_name + self.attrdict = attrdict + self.address = address + + def __repr__(self): + kwargs = ', '.join(["%s=%r" % (arg, val) + for arg, val in self.attrdict.iteritems()]) + return '<%s(%s) at remote 0x%x>' % (self.cl_name, + kwargs, self.address) + + +def _PyObject_VAR_SIZE(typeobj, nitems): + return ( ( typeobj.field('tp_basicsize') + + nitems * typeobj.field('tp_itemsize') + + (SIZEOF_VOID_P - 1) + ) & ~(SIZEOF_VOID_P - 1) + ).cast(_type_size_t) + +class HeapTypeObjectPtr(PyObjectPtr): + _typename = 'PyObject' + + def proxyval(self): + ''' + Support for new-style classes. + + Currently we just locate the dictionary using _PyObject_GetDictPtr, + ignoring descriptors + ''' + attr_dict = {} + + try: + typeobj = self.type() + dictoffset = int_from_int(typeobj.field('tp_dictoffset')) + if dictoffset != 0: + if dictoffset < 0: + type_PyVarObject_ptr = gdb.lookup_type('PyVarObject').pointer() + tsize = int_from_int(self._gdbval.cast(type_PyVarObject_ptr)['ob_size']) + if tsize < 0: + tsize = -tsize + size = _PyObject_VAR_SIZE(typeobj, tsize) + dictoffset += size + assert dictoffset > 0 + assert dictoffset % SIZEOF_VOID_P == 0 + + dictptr = self._gdbval.cast(_type_char_ptr) + dictoffset + PyObjectPtrPtr = PyObjectPtr.get_gdb_type().pointer() + dictptr = dictptr.cast(PyObjectPtrPtr) + attr_dict = PyObjectPtr.from_pyobject_ptr(dictptr.dereference()).proxyval() + except RuntimeError: + # Corrupt data somewhere; fail safe + pass + + tp_name = self.safe_tp_name() + + # New-style class: + return InstanceProxy(tp_name, attr_dict, long(self._gdbval)) + + class PyBoolObjectPtr(PyObjectPtr): """ Class wrapping a gdb.Value that's a PyBoolObject* i.e. one of the two @@ -244,6 +340,7 @@ class PyBoolObjectPtr(PyObjectPtr): else: return False + class PyClassObjectPtr(PyObjectPtr): """ Class wrapping a gdb.Value that's a PyClassObject* i.e. a <classobj> @@ -251,6 +348,7 @@ class PyClassObjectPtr(PyObjectPtr): """ _typename = 'PyClassObject' + class PyCodeObjectPtr(PyObjectPtr): """ Class wrapping a gdb.Value that's a PyCodeObject* i.e. a <code> instance @@ -258,6 +356,7 @@ class PyCodeObjectPtr(PyObjectPtr): """ _typename = 'PyCodeObject' + class PyDictObjectPtr(PyObjectPtr): """ Class wrapping a gdb.Value that's a PyDictObject* i.e. a dict instance @@ -267,7 +366,7 @@ class PyDictObjectPtr(PyObjectPtr): def proxyval(self): result = {} - for i in safe_range(self.field('ma_mask')+1): + for i in safe_range(self.field('ma_mask') + 1): ep = self.field('ma_table') + i pvalue = PyObjectPtr.from_pyobject_ptr(ep['me_value']) if not pvalue.is_null(): @@ -275,20 +374,11 @@ class PyDictObjectPtr(PyObjectPtr): result[pkey.proxyval()] = pvalue.proxyval() return result + class PyInstanceObjectPtr(PyObjectPtr): _typename = 'PyInstanceObject' def proxyval(self): - class InstanceProxy(object): - def __init__(self, cl_name, attrdict, address): - self.cl_name = cl_name - self.attrdict = attrdict - self.address = address - - def __repr__(self): - kwargs = ', '.join(["%s=%r"%(arg, val) for arg, val in self.attrdict.iteritems()]) - return '<%s(%s) at remote 0x%x>' % (self.cl_name, kwargs, self.address) - # Get name of class: in_class = PyObjectPtr.from_pyobject_ptr(self.field('in_class')) cl_name = PyObjectPtr.from_pyobject_ptr(in_class.field('cl_name')).proxyval() @@ -296,8 +386,10 @@ class PyInstanceObjectPtr(PyObjectPtr): # Get dictionary of instance attributes: in_dict = PyObjectPtr.from_pyobject_ptr(self.field('in_dict')).proxyval() + # Old-style class: return InstanceProxy(cl_name, in_dict, long(self._gdbval)) + class PyIntObjectPtr(PyObjectPtr): _typename = 'PyIntObject' @@ -305,6 +397,7 @@ class PyIntObjectPtr(PyObjectPtr): result = int_from_int(self.field('ob_ival')) return result + class PyListObjectPtr(PyObjectPtr): _typename = 'PyListObject' @@ -318,6 +411,7 @@ class PyListObjectPtr(PyObjectPtr): for i in safe_range(int_from_int(self.field('ob_size')))] return result + class PyLongObjectPtr(PyObjectPtr): _typename = 'PyLongObject' @@ -329,7 +423,7 @@ class PyLongObjectPtr(PyObjectPtr): digit ob_digit[1]; }; - with this description: + with this description: The absolute value of a number is equal to SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i) Negative numbers are represented with ob_size < 0; @@ -351,34 +445,37 @@ class PyLongObjectPtr(PyObjectPtr): # FIXME: I haven't yet tested this case SHIFT = 30L - digits = [long(ob_digit[i]) * 2**(SHIFT*i) for i in safe_range(abs(ob_size))] + digits = [long(ob_digit[i]) * 2**(SHIFT*i) + for i in safe_range(abs(ob_size))] result = sum(digits) if ob_size < 0: result = -result return result + class PyNoneStructPtr(PyObjectPtr): """ Class wrapping a gdb.Value that's a PyObject* pointing to the singleton (we hope) _Py_NoneStruct with ob_type PyNone_Type """ _typename = 'PyObject' + def proxyval(self): return None + class PyFrameObjectPtr(PyObjectPtr): _typename = 'PyFrameObject' + def __str__(self): fi = FrameInfo(self) return str(fi) + class PyStringObjectPtr(PyObjectPtr): _typename = 'PyStringObject' def __str__(self): - # Lookup the gdb.Type for "char*" - _type_char_ptr = gdb.lookup_type('char').pointer() - field_ob_sval = self.field('ob_sval') char_ptr = field_ob_sval.address.cast(_type_char_ptr) return char_ptr.string() @@ -386,6 +483,7 @@ class PyStringObjectPtr(PyObjectPtr): def proxyval(self): return str(self) + class PyTupleObjectPtr(PyObjectPtr): _typename = 'PyTupleObject' @@ -399,9 +497,11 @@ class PyTupleObjectPtr(PyObjectPtr): for i in safe_range(int_from_int(self.field('ob_size')))]) return result + class PyTypeObjectPtr(PyObjectPtr): _typename = 'PyTypeObject' + class PyUnicodeObjectPtr(PyObjectPtr): _typename = 'PyUnicodeObject' @@ -421,9 +521,11 @@ class PyUnicodeObjectPtr(PyObjectPtr): result = u''.join([unichr(ucs) for ucs in Py_UNICODEs]) return result + def int_from_int(gdbval): return int(str(gdbval)) + def stringify(val): # TODO: repr() puts everything on one line; pformat can be nicer, but # can lead to v.long results; this function isolates the choice @@ -433,6 +535,7 @@ def stringify(val): from pprint import pformat return pformat(val) + class FrameInfo: ''' Class representing all of the information we can scrape about a @@ -457,7 +560,7 @@ class FrameInfo: value = value.proxyval() #print 'value=%s' % value self.locals.append((str(name), value)) - + def __str__(self): return ('File %s, line %i, in %s (%s)' % (self.co_filename, @@ -466,6 +569,7 @@ class FrameInfo: ', '.join(['%s=%s' % (k, stringify(v)) for k, v in self.locals])) ) + class PyObjectPtrPrinter: "Prints a (PyObject*)" @@ -476,6 +580,7 @@ class PyObjectPtrPrinter: proxyval = PyObjectPtr.from_pyobject_ptr(self.gdbval).proxyval() return stringify(proxyval) + class PyFrameObjectPtrPrinter(PyObjectPtrPrinter): "Prints a (PyFrameObject*)" @@ -484,6 +589,7 @@ class PyFrameObjectPtrPrinter(PyObjectPtrPrinter): fi = FrameInfo(pyop) return str(fi) + def pretty_printer_lookup(gdbval): type = gdbval.type.unqualified() if type.code == gdb.TYPE_CODE_PTR: @@ -551,8 +657,7 @@ def pybt(): print fi, except RuntimeError: print '(unable to print python frame; corrupt data?)', - - + for i, gdbframe in enumerate(gdb.selected_thread().frames()): #print dir(gdbframe), gdbframe.name() if 'PyEval_EvalFrameEx' == gdbframe.name(): diff --git a/test_gdb.py b/test_gdb.py new file mode 100644 index 0000000..a75a2b9 --- /dev/null +++ b/test_gdb.py @@ -0,0 +1,265 @@ +# Verify that gdb can pretty-print the various PyObject* types +# +# The code for testing gdb was adapted from similar work in Unladen Swallow's +# Lib/test/test_jit_gdb.py + +import os +import re +import subprocess +import sys +import unittest + +from test.test_support import run_unittest, TestSkipped + +try: + gdb_version, _ = subprocess.Popen(["gdb", "--version"], + stdout=subprocess.PIPE).communicate() +except OSError: + # This is what "no gdb" looks like. There may, however, be other + # errors that manifest this way too. + raise TestSkipped("Couldn't find gdb on the path") +gdb_version_number = re.search(r"^GNU gdb [^\d]*(\d+)\.", gdb_version) +if int(gdb_version_number.group(1)) < 7: + raise TestSkipped("gdb versions before 7.0 didn't support python embedding" + " Saw:\n" + gdb_version) + +# Verify that "gdb" was built with the embedded python support enabled: +cmd = "--eval-command=python import sys; print sys.version_info" +p = subprocess.Popen(["gdb", "--batch", cmd], + stdout=subprocess.PIPE) +gdbpy_version, _ = p.communicate() +if gdbpy_version == '': + raise TestSkipped("gdb not built with embedded python support") + + +class DebuggerTests(unittest.TestCase): + + """Test that the debugger can debug Python.""" + + def run_gdb(self, *args): + """Runs gdb with the command line given by *args. + + Returns its stdout, stderr + """ + out, err = subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ).communicate() + return out, err + + def get_stack_trace(self, source, breakpoint='PyObject_Print', + commands_after_breakpoint=None): + ''' + Run 'python -c SOURCE' under gdb with a breakpoint. + + Support injecting commands after the breakpoint is reached + + Returns the stdout from gdb + + commands_after_breakpoint: if provided, a list of strings: gdb commands + ''' + # We use "set breakpoint pending yes" to avoid blocking with a: + # Function "foo" not defined. + # Make breakpoint pending on future shared library load? (y or [n]) + # error, which typically happens python is dynamically linked (the + # breakpoints of interest are to be found in the shared library) + # When this happens, we still get: + # Function "PyObject_Print" not defined. + # emitted to stderr each time, alas. + + # Initially I had "--eval-command=continue" here, but removed it to + # avoid repeated print breakpoints when traversing hierarchical data + # structures + + # Generate a list of commands in gdb's language: + commands = ['set breakpoint pending yes', + 'break %s' % breakpoint, + 'run'] + if commands_after_breakpoint: + commands += commands_after_breakpoint + commands += ['backtrace'] + + # print commands + + # Use "commands" to generate the arguments with which to invoke "gdb": + args = ["gdb", "--batch"] + args += ['--eval-command=%s' % cmd for cmd in commands] + args += ["--args", + sys.executable, "-S", "-c", source] + # -S suppresses the default 'import site' + + # print args + + # Use "args" to invoke gdb, capturing stdout, stderr: + out, err = self.run_gdb(*args) + + # Ignore some noise on stderr due to the pending breakpoint: + err = err.replace('Function "%s" not defined.\n' % breakpoint, '') + + # Ensure no unexpected error messages: + self.assertEquals(err, '') + + return out + + def get_gdb_repr(self, source, + commands_after_breakpoint=None): + # Given an input python source representation of data, + # run "python -c'print DATA'" under gdb with a breakpoint on + # PyObject_Print and scrape out gdb's representation of the "op" + # parameter, and verify that the gdb displays the same string + # + # For a nested structure, the first time we hit the breakpoint will + # give us the top-level structure + gdb_output = self.get_stack_trace(source, 'PyObject_Print', + commands_after_breakpoint) + m = re.match('.*#0 PyObject_Print \(op\=(.*?), fp=.*\).*', + gdb_output, re.DOTALL) + #print m.groups() + return m.group(1), gdb_output + + def test_getting_backtrace(self): + gdb_output = self.get_stack_trace('print 42') + self.assertTrue('PyObject_Print' in gdb_output) + + def assertGdbRepr(self, val, commands_after_breakpoint=None): + # Ensure that gdb's rendering of the value in a debugged process + # matches repr(value) in this process: + gdb_repr, gdb_output = self.get_gdb_repr('print ' + repr(val), + commands_after_breakpoint) + self.assertEquals(gdb_repr, repr(val), gdb_output) + + def test_int(self): + self.assertGdbRepr(42) + self.assertGdbRepr(0) + self.assertGdbRepr(-7) + self.assertGdbRepr(sys.maxint) + self.assertGdbRepr(-sys.maxint) + + def test_long(self): + self.assertGdbRepr(0L) + self.assertGdbRepr(1000000000000L) + self.assertGdbRepr(-1L) + self.assertGdbRepr(-1000000000000000L) + + def test_singletons(self): + self.assertGdbRepr(True) + self.assertGdbRepr(False) + self.assertGdbRepr(None) + + def test_dicts(self): + self.assertGdbRepr({}) + self.assertGdbRepr({'foo': 'bar'}) + + def test_lists(self): + self.assertGdbRepr([]) + self.assertGdbRepr(range(5)) + + def test_strings(self): + self.assertGdbRepr('') + self.assertGdbRepr('And now for something hopefully the same') + + def test_tuples(self): + self.assertGdbRepr(tuple()) + self.assertGdbRepr((1,)) + + def test_unicode(self): + self.assertGdbRepr(u'hello world') + self.assertGdbRepr(u'\u2620') + + def test_classic_class(self): + gdb_repr, gdb_output = self.get_gdb_repr(''' +class Foo: + pass +foo = Foo() +foo.an_int = 42 +print foo''') + # FIXME: is there an "assertMatches"; should there be? + m = re.match(r'<Foo\(an_int=42\) at remote 0x[0-9a-f]+>', gdb_repr) + self.assertTrue(m, + msg='Unexpected classic-class rendering %r' % gdb_repr) + + def test_modern_class(self): + gdb_repr, gdb_output = self.get_gdb_repr(''' +class Foo(object): + pass +foo = Foo() +foo.an_int = 42 +print foo''') + # FIXME: is there an "assertMatches"; should there be? + m = re.match(r'<Foo\(an_int=42\) at remote 0x[0-9a-f]+>', gdb_repr) + self.assertTrue(m, + msg='Unexpected new-style class rendering %r' % gdb_repr) + + def test_subclassing_list(self): + gdb_repr, gdb_output = self.get_gdb_repr(''' +class Foo(list): + pass +foo = Foo() +foo += [1, 2, 3] +foo.an_int = 42 +print foo''') + # FIXME: is there an "assertMatches"; should there be? + m = re.match(r'<Foo\(an_int=42\) at remote 0x[0-9a-f]+>', gdb_repr) + self.assertTrue(m, + msg='Unexpected new-style class rendering %r' % gdb_repr) + + def test_subclassing_tuple(self): + '''This should exercise the negative tp_dictoffset code in the + new-style class support''' + gdb_repr, gdb_output = self.get_gdb_repr(''' +class Foo(tuple): + pass +foo = Foo((1, 2, 3)) +foo.an_int = 42 +print foo''') + # FIXME: is there an "assertMatches"; should there be? + m = re.match(r'<Foo\(an_int=42\) at remote 0x[0-9a-f]+>', gdb_repr) + self.assertTrue(m, + msg='Unexpected new-style class rendering %r' % gdb_repr) + + def assertSane(self, source, corruption, exp_type='unknown'): + '''Run Python under gdb, corrupting variables in the inferior process + immediately before taking a backtrace. + + Verify that the variable's representation is the expected failsafe + representation''' + gdb_repr, gdb_output = \ + self.get_gdb_repr(source, + commands_after_breakpoint=[corruption]) + self.assertTrue(re.match('<%s at remote 0x[0-9a-f]+>' % exp_type, + gdb_repr), + 'Unexpected gdb representation: %r\n%s' % \ + (gdb_repr, gdb_output)) + + def test_NULL_ptr(self): + 'Ensure that a NULL PyObject* is handled gracefully' + self.assertSane('print 42', + 'set variable op=0') + + def test_NULL_ob_type(self): + self.assertSane('print 42', + 'set op->ob_type=0') + + def test_corrupt_ob_type(self): + self.assertSane('print 42', + 'set op->ob_type=0xDEADBEEF') + + def test_corrupt_tp_flags(self): + self.assertSane('print 42', + 'set op->ob_type->tp_flags=0x0', + exp_type='int') + + def test_corrupt_tp_name(self): + self.assertSane('print 42', + 'set op->ob_type->tp_name=0xDEADBEEF') + + # TODO: + # frames + + +def test_main(): + #run_unittest(DebuggerTests) + unittest.main() + + +if __name__ == "__main__": + test_main() |