Home | History | Annotate | Download | only in Tests
      1 """
      2 Tests that run inside GDB.
      3 
      4 Note: debug information is already imported by the file generated by
      5 Cython.Debugger.Cygdb.make_command_file()
      6 """
      7 
      8 import os
      9 import re
     10 import sys
     11 import trace
     12 import inspect
     13 import warnings
     14 import unittest
     15 import textwrap
     16 import tempfile
     17 import functools
     18 import traceback
     19 import itertools
     20 from test import test_support
     21 
     22 import gdb
     23 
     24 from Cython.Debugger import libcython
     25 from Cython.Debugger import libpython
     26 from Cython.Debugger.Tests import TestLibCython as test_libcython
     27 
     28 # for some reason sys.argv is missing in gdb
     29 sys.argv = ['gdb']
     30 
     31 
     32 def print_on_call_decorator(func):
     33     @functools.wraps(func)
     34     def wrapper(self, *args, **kwargs):
     35         _debug(type(self).__name__, func.__name__)
     36 
     37         try:
     38             return func(self, *args, **kwargs)
     39         except Exception, e:
     40             _debug("An exception occurred:", traceback.format_exc(e))
     41             raise
     42 
     43     return wrapper
     44 
     45 class TraceMethodCallMeta(type):
     46 
     47     def __init__(self, name, bases, dict):
     48         for func_name, func in dict.iteritems():
     49             if inspect.isfunction(func):
     50                 setattr(self, func_name, print_on_call_decorator(func))
     51 
     52 
     53 class DebugTestCase(unittest.TestCase):
     54     """
     55     Base class for test cases. On teardown it kills the inferior and unsets
     56     all breakpoints.
     57     """
     58 
     59     __metaclass__ = TraceMethodCallMeta
     60 
     61     def __init__(self, name):
     62         super(DebugTestCase, self).__init__(name)
     63         self.cy = libcython.cy
     64         self.module = libcython.cy.cython_namespace['codefile']
     65         self.spam_func, self.spam_meth = libcython.cy.functions_by_name['spam']
     66         self.ham_func = libcython.cy.functions_by_qualified_name[
     67             'codefile.ham']
     68         self.eggs_func = libcython.cy.functions_by_qualified_name[
     69             'codefile.eggs']
     70 
     71     def read_var(self, varname, cast_to=None):
     72         result = gdb.parse_and_eval('$cy_cvalue("%s")' % varname)
     73         if cast_to:
     74             result = cast_to(result)
     75 
     76         return result
     77 
     78     def local_info(self):
     79         return gdb.execute('info locals', to_string=True)
     80 
     81     def lineno_equals(self, source_line=None, lineno=None):
     82         if source_line is not None:
     83             lineno = test_libcython.source_to_lineno[source_line]
     84         frame = gdb.selected_frame()
     85         self.assertEqual(libcython.cython_info.lineno(frame), lineno)
     86 
     87     def break_and_run(self, source_line):
     88         break_lineno = test_libcython.source_to_lineno[source_line]
     89         gdb.execute('cy break codefile:%d' % break_lineno, to_string=True)
     90         gdb.execute('run', to_string=True)
     91 
     92     def tearDown(self):
     93         gdb.execute('delete breakpoints', to_string=True)
     94         try:
     95             gdb.execute('kill inferior 1', to_string=True)
     96         except RuntimeError:
     97             pass
     98 
     99         gdb.execute('set args -c "import codefile"')
    100 
    101 
    102 class TestDebugInformationClasses(DebugTestCase):
    103 
    104     def test_CythonModule(self):
    105         "test that debug information was parsed properly into data structures"
    106         self.assertEqual(self.module.name, 'codefile')
    107         global_vars = ('c_var', 'python_var', '__name__',
    108                        '__builtins__', '__doc__', '__file__')
    109         assert set(global_vars).issubset(self.module.globals)
    110 
    111     def test_CythonVariable(self):
    112         module_globals = self.module.globals
    113         c_var = module_globals['c_var']
    114         python_var = module_globals['python_var']
    115         self.assertEqual(c_var.type, libcython.CObject)
    116         self.assertEqual(python_var.type, libcython.PythonObject)
    117         self.assertEqual(c_var.qualified_name, 'codefile.c_var')
    118 
    119     def test_CythonFunction(self):
    120         self.assertEqual(self.spam_func.qualified_name, 'codefile.spam')
    121         self.assertEqual(self.spam_meth.qualified_name,
    122                          'codefile.SomeClass.spam')
    123         self.assertEqual(self.spam_func.module, self.module)
    124 
    125         assert self.eggs_func.pf_cname, (self.eggs_func, self.eggs_func.pf_cname)
    126         assert not self.ham_func.pf_cname
    127         assert not self.spam_func.pf_cname
    128         assert not self.spam_meth.pf_cname
    129 
    130         self.assertEqual(self.spam_func.type, libcython.CObject)
    131         self.assertEqual(self.ham_func.type, libcython.CObject)
    132 
    133         self.assertEqual(self.spam_func.arguments, ['a'])
    134         self.assertEqual(self.spam_func.step_into_functions,
    135                          set(['puts', 'some_c_function']))
    136 
    137         expected_lineno = test_libcython.source_to_lineno['def spam(a=0):']
    138         self.assertEqual(self.spam_func.lineno, expected_lineno)
    139         self.assertEqual(sorted(self.spam_func.locals), list('abcd'))
    140 
    141 
    142 class TestParameters(unittest.TestCase):
    143 
    144     def test_parameters(self):
    145         gdb.execute('set cy_colorize_code on')
    146         assert libcython.parameters.colorize_code
    147         gdb.execute('set cy_colorize_code off')
    148         assert not libcython.parameters.colorize_code
    149 
    150 
    151 class TestBreak(DebugTestCase):
    152 
    153     def test_break(self):
    154         breakpoint_amount = len(gdb.breakpoints() or ())
    155         gdb.execute('cy break codefile.spam')
    156 
    157         self.assertEqual(len(gdb.breakpoints()), breakpoint_amount + 1)
    158         bp = gdb.breakpoints()[-1]
    159         self.assertEqual(bp.type, gdb.BP_BREAKPOINT)
    160         assert self.spam_func.cname in bp.location
    161         assert bp.enabled
    162 
    163     def test_python_break(self):
    164         gdb.execute('cy break -p join')
    165         assert 'def join(' in gdb.execute('cy run', to_string=True)
    166 
    167     def test_break_lineno(self):
    168         beginline = 'import os'
    169         nextline = 'cdef int c_var = 12'
    170 
    171         self.break_and_run(beginline)
    172         self.lineno_equals(beginline)
    173         step_result = gdb.execute('cy step', to_string=True)
    174         self.lineno_equals(nextline)
    175         assert step_result.rstrip().endswith(nextline)
    176 
    177 
    178 class TestKilled(DebugTestCase):
    179 
    180     def test_abort(self):
    181         gdb.execute("set args -c 'import os; os.abort()'")
    182         output = gdb.execute('cy run', to_string=True)
    183         assert 'abort' in output.lower()
    184 
    185 
    186 class DebugStepperTestCase(DebugTestCase):
    187 
    188     def step(self, varnames_and_values, source_line=None, lineno=None):
    189         gdb.execute(self.command)
    190         for varname, value in varnames_and_values:
    191             self.assertEqual(self.read_var(varname), value, self.local_info())
    192 
    193         self.lineno_equals(source_line, lineno)
    194 
    195 
    196 class TestStep(DebugStepperTestCase):
    197     """
    198     Test stepping. Stepping happens in the code found in
    199     Cython/Debugger/Tests/codefile.
    200     """
    201 
    202     def test_cython_step(self):
    203         gdb.execute('cy break codefile.spam')
    204 
    205         gdb.execute('run', to_string=True)
    206         self.lineno_equals('def spam(a=0):')
    207 
    208         gdb.execute('cy step', to_string=True)
    209         self.lineno_equals('b = c = d = 0')
    210 
    211         self.command = 'cy step'
    212         self.step([('b', 0)], source_line='b = 1')
    213         self.step([('b', 1), ('c', 0)], source_line='c = 2')
    214         self.step([('c', 2)], source_line='int(10)')
    215         self.step([], source_line='puts("spam")')
    216 
    217         gdb.execute('cont', to_string=True)
    218         self.assertEqual(len(gdb.inferiors()), 1)
    219         self.assertEqual(gdb.inferiors()[0].pid, 0)
    220 
    221     def test_c_step(self):
    222         self.break_and_run('some_c_function()')
    223         gdb.execute('cy step', to_string=True)
    224         self.assertEqual(gdb.selected_frame().name(), 'some_c_function')
    225 
    226     def test_python_step(self):
    227         self.break_and_run('os.path.join("foo", "bar")')
    228 
    229         result = gdb.execute('cy step', to_string=True)
    230 
    231         curframe = gdb.selected_frame()
    232         self.assertEqual(curframe.name(), 'PyEval_EvalFrameEx')
    233 
    234         pyframe = libpython.Frame(curframe).get_pyop()
    235         # With Python 3 inferiors, pyframe.co_name will return a PyUnicodePtr,
    236         # be compatible
    237         frame_name = pyframe.co_name.proxyval(set())
    238         self.assertEqual(frame_name, 'join')
    239         assert re.match(r'\d+    def join\(', result), result
    240 
    241 
    242 class TestNext(DebugStepperTestCase):
    243 
    244     def test_cython_next(self):
    245         self.break_and_run('c = 2')
    246 
    247         lines = (
    248             'int(10)',
    249             'puts("spam")',
    250             'os.path.join("foo", "bar")',
    251             'some_c_function()',
    252         )
    253 
    254         for line in lines:
    255             gdb.execute('cy next')
    256             self.lineno_equals(line)
    257 
    258 
    259 class TestLocalsGlobals(DebugTestCase):
    260 
    261     def test_locals(self):
    262         self.break_and_run('int(10)')
    263 
    264         result = gdb.execute('cy locals', to_string=True)
    265         assert 'a = 0', repr(result)
    266         assert 'b = (int) 1', result
    267         assert 'c = (int) 2' in result, repr(result)
    268 
    269     def test_globals(self):
    270         self.break_and_run('int(10)')
    271 
    272         result = gdb.execute('cy globals', to_string=True)
    273         assert '__name__ ' in result, repr(result)
    274         assert '__doc__ ' in result, repr(result)
    275         assert 'os ' in result, repr(result)
    276         assert 'c_var ' in result, repr(result)
    277         assert 'python_var ' in result, repr(result)
    278 
    279 
    280 class TestBacktrace(DebugTestCase):
    281 
    282     def test_backtrace(self):
    283         libcython.parameters.colorize_code.value = False
    284 
    285         self.break_and_run('os.path.join("foo", "bar")')
    286 
    287         def match_backtrace_output(result):
    288             assert re.search(r'\#\d+ *0x.* in spam\(\) at .*codefile\.pyx:22',
    289                              result), result
    290             assert 'os.path.join("foo", "bar")' in result, result
    291 
    292         result = gdb.execute('cy bt', to_string=True)
    293         match_backtrace_output(result)
    294 
    295         result = gdb.execute('cy bt -a', to_string=True)
    296         match_backtrace_output(result)
    297 
    298         # Apparently not everyone has main()
    299         # assert re.search(r'\#0 *0x.* in main\(\)', result), result
    300 
    301 
    302 class TestFunctions(DebugTestCase):
    303 
    304     def test_functions(self):
    305         self.break_and_run('c = 2')
    306         result = gdb.execute('print $cy_cname("b")', to_string=True)
    307         assert re.search('__pyx_.*b', result), result
    308 
    309         result = gdb.execute('print $cy_lineno()', to_string=True)
    310         supposed_lineno = test_libcython.source_to_lineno['c = 2']
    311         assert str(supposed_lineno) in result, (supposed_lineno, result)
    312 
    313         result = gdb.execute('print $cy_cvalue("b")', to_string=True)
    314         assert '= 1' in result
    315 
    316 
    317 class TestPrint(DebugTestCase):
    318 
    319     def test_print(self):
    320         self.break_and_run('c = 2')
    321         result = gdb.execute('cy print b', to_string=True)
    322         self.assertEqual('b = (int) 1\n', result)
    323 
    324 
    325 class TestUpDown(DebugTestCase):
    326 
    327     def test_updown(self):
    328         self.break_and_run('os.path.join("foo", "bar")')
    329         gdb.execute('cy step')
    330         self.assertRaises(RuntimeError, gdb.execute, 'cy down')
    331 
    332         result = gdb.execute('cy up', to_string=True)
    333         assert 'spam()' in result
    334         assert 'os.path.join("foo", "bar")' in result
    335 
    336 
    337 class TestExec(DebugTestCase):
    338 
    339     def setUp(self):
    340         super(TestExec, self).setUp()
    341         self.fd, self.tmpfilename = tempfile.mkstemp()
    342         self.tmpfile = os.fdopen(self.fd, 'r+')
    343 
    344     def tearDown(self):
    345         super(TestExec, self).tearDown()
    346 
    347         try:
    348             self.tmpfile.close()
    349         finally:
    350             os.remove(self.tmpfilename)
    351 
    352     def eval_command(self, command):
    353         gdb.execute('cy exec open(%r, "w").write(str(%s))' %
    354                                                 (self.tmpfilename, command))
    355         return self.tmpfile.read().strip()
    356 
    357     def test_cython_exec(self):
    358         self.break_and_run('os.path.join("foo", "bar")')
    359 
    360         # test normal behaviour
    361         self.assertEqual("[0]", self.eval_command('[a]'))
    362 
    363         # test multiline code
    364         result = gdb.execute(textwrap.dedent('''\
    365             cy exec
    366             pass
    367 
    368             "nothing"
    369             end
    370             '''))
    371         result = self.tmpfile.read().rstrip()
    372         self.assertEqual('', result)
    373 
    374     def test_python_exec(self):
    375         self.break_and_run('os.path.join("foo", "bar")')
    376         gdb.execute('cy step')
    377 
    378         gdb.execute('cy exec some_random_var = 14')
    379         self.assertEqual('14', self.eval_command('some_random_var'))
    380 
    381 
    382 class CySet(DebugTestCase):
    383 
    384     def test_cyset(self):
    385         self.break_and_run('os.path.join("foo", "bar")')
    386 
    387         gdb.execute('cy set a = $cy_eval("{None: []}")')
    388         stringvalue = self.read_var("a", cast_to=str)
    389         self.assertEqual(stringvalue, "{None: []}")
    390 
    391 
    392 class TestCyEval(DebugTestCase):
    393     "Test the $cy_eval() gdb function."
    394 
    395     def test_cy_eval(self):
    396         # This function leaks a few objects in the GDB python process. This
    397         # is no biggie
    398         self.break_and_run('os.path.join("foo", "bar")')
    399 
    400         result = gdb.execute('print $cy_eval("None")', to_string=True)
    401         assert re.match(r'\$\d+ = None\n', result), result
    402 
    403         result = gdb.execute('print $cy_eval("[a]")', to_string=True)
    404         assert re.match(r'\$\d+ = \[0\]', result), result
    405 
    406 
    407 class TestClosure(DebugTestCase):
    408 
    409     def break_and_run_func(self, funcname):
    410         gdb.execute('cy break ' + funcname)
    411         gdb.execute('cy run')
    412 
    413     def test_inner(self):
    414         self.break_and_run_func('inner')
    415         self.assertEqual('', gdb.execute('cy locals', to_string=True))
    416 
    417         # Allow the Cython-generated code to initialize the scope variable
    418         gdb.execute('cy step')
    419 
    420         self.assertEqual(str(self.read_var('a')), "'an object'")
    421         print_result = gdb.execute('cy print a', to_string=True).strip()
    422         self.assertEqual(print_result, "a = 'an object'")
    423 
    424     def test_outer(self):
    425         self.break_and_run_func('outer')
    426         self.assertEqual('', gdb.execute('cy locals', to_string=True))
    427 
    428         # Initialize scope with 'a' uninitialized
    429         gdb.execute('cy step')
    430         self.assertEqual('', gdb.execute('cy locals', to_string=True))
    431 
    432         # Initialize 'a' to 1
    433         gdb.execute('cy step')
    434         print_result = gdb.execute('cy print a', to_string=True).strip()
    435         self.assertEqual(print_result, "a = 'an object'")
    436 
    437 
    438 _do_debug = os.environ.get('GDB_DEBUG')
    439 if _do_debug:
    440     _debug_file = open('/dev/tty', 'w')
    441 
    442 def _debug(*messages):
    443     if _do_debug:
    444         messages = itertools.chain([sys._getframe(1).f_code.co_name, ':'],
    445                                    messages)
    446         _debug_file.write(' '.join(str(msg) for msg in messages) + '\n')
    447 
    448 
    449 def run_unittest_in_module(modulename):
    450     try:
    451         gdb.lookup_type('PyModuleObject')
    452     except RuntimeError:
    453         msg = ("Unable to run tests, Python was not compiled with "
    454                 "debugging information. Either compile python with "
    455                 "-g or get a debug build (configure with --with-pydebug).")
    456         warnings.warn(msg)
    457         os._exit(1)
    458     else:
    459         m = __import__(modulename, fromlist=[''])
    460         tests = inspect.getmembers(m, inspect.isclass)
    461 
    462         # test_support.run_unittest(tests)
    463 
    464         test_loader = unittest.TestLoader()
    465         suite = unittest.TestSuite(
    466             [test_loader.loadTestsFromTestCase(cls) for name, cls in tests])
    467 
    468         result = unittest.TextTestRunner(verbosity=1).run(suite)
    469         return result.wasSuccessful()
    470 
    471 def runtests():
    472     """
    473     Run the libcython and libpython tests. Ensure that an appropriate status is
    474     returned to the parent test process.
    475     """
    476     from Cython.Debugger.Tests import test_libpython_in_gdb
    477 
    478     success_libcython = run_unittest_in_module(__name__)
    479     success_libpython = run_unittest_in_module(test_libpython_in_gdb.__name__)
    480 
    481     if not success_libcython or not success_libpython:
    482         sys.exit(2)
    483 
    484 def main(version, trace_code=False):
    485     global inferior_python_version
    486 
    487     inferior_python_version = version
    488 
    489     if trace_code:
    490         tracer = trace.Trace(count=False, trace=True, outfile=sys.stderr,
    491                             ignoredirs=[sys.prefix, sys.exec_prefix])
    492         tracer.runfunc(runtests)
    493     else:
    494         runtests()
    495