# 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'', 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'', 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'', 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'', 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()