diff options
Diffstat (limited to 'test_gdb.py')
-rw-r--r-- | test_gdb.py | 265 |
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() |