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