summaryrefslogtreecommitdiffstats
path: root/test_gdb.py
diff options
context:
space:
mode:
Diffstat (limited to 'test_gdb.py')
-rw-r--r--test_gdb.py265
1 files changed, 265 insertions, 0 deletions
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()