Fix Python 3 testrunner and give a warning for Python 2 builds without debug symbols
[cython.git] / Cython / Debugger / Tests / test_libcython_in_gdb.py
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         self.assertEqual(str(pyframe.co_name), 'join')
236         assert re.match(r'\d+    def join\(', result), result
237
238
239 class TestNext(DebugStepperTestCase):
240
241     def test_cython_next(self):
242         self.break_and_run('c = 2')
243
244         lines = (
245             'int(10)',
246             'puts("spam")',
247             'os.path.join("foo", "bar")',
248             'some_c_function()',
249         )
250
251         for line in lines:
252             gdb.execute('cy next')
253             self.lineno_equals(line)
254
255
256 class TestLocalsGlobals(DebugTestCase):
257
258     def test_locals(self):
259         self.break_and_run('int(10)')
260
261         result = gdb.execute('cy locals', to_string=True)
262         assert 'a = 0', repr(result)
263         assert 'b = (int) 1', result
264         assert 'c = (int) 2' in result, repr(result)
265
266     def test_globals(self):
267         self.break_and_run('int(10)')
268
269         result = gdb.execute('cy globals', to_string=True)
270         assert '__name__ ' in result, repr(result)
271         assert '__doc__ ' in result, repr(result)
272         assert 'os ' in result, repr(result)
273         assert 'c_var ' in result, repr(result)
274         assert 'python_var ' in result, repr(result)
275
276
277 class TestBacktrace(DebugTestCase):
278
279     def test_backtrace(self):
280         libcython.parameters.colorize_code.value = False
281
282         self.break_and_run('os.path.join("foo", "bar")')
283         result = gdb.execute('cy bt', to_string=True)
284
285         _debug(libpython.execute, libpython._execute, gdb.execute)
286         _debug(gdb.execute('cy list', to_string=True))
287         _debug(repr(result))
288
289         assert re.search(r'\#\d+ *0x.* in spam\(\) at .*codefile\.pyx:22',
290                          result), result
291         assert 'os.path.join("foo", "bar")' in result, result
292
293         gdb.execute("cy step")
294
295         gdb.execute('cy bt')
296         result = gdb.execute('cy bt -a', to_string=True)
297         assert re.search(r'\#0 *0x.* in main\(\)', result), result
298
299
300 class TestFunctions(DebugTestCase):
301
302     def test_functions(self):
303         self.break_and_run('c = 2')
304         result = gdb.execute('print $cy_cname("b")', to_string=True)
305         assert re.search('__pyx_.*b', result), result
306
307         result = gdb.execute('print $cy_lineno()', to_string=True)
308         supposed_lineno = test_libcython.source_to_lineno['c = 2']
309         assert str(supposed_lineno) in result, (supposed_lineno, result)
310
311         result = gdb.execute('print $cy_cvalue("b")', to_string=True)
312         assert '= 1' in result
313
314
315 class TestPrint(DebugTestCase):
316
317     def test_print(self):
318         self.break_and_run('c = 2')
319         result = gdb.execute('cy print b', to_string=True)
320         self.assertEqual('b = (int) 1\n', result)
321
322
323 class TestUpDown(DebugTestCase):
324
325     def test_updown(self):
326         self.break_and_run('os.path.join("foo", "bar")')
327         gdb.execute('cy step')
328         self.assertRaises(RuntimeError, gdb.execute, 'cy down')
329
330         result = gdb.execute('cy up', to_string=True)
331         assert 'spam()' in result
332         assert 'os.path.join("foo", "bar")' in result
333
334
335 class TestExec(DebugTestCase):
336
337     def setUp(self):
338         super(TestExec, self).setUp()
339         self.fd, self.tmpfilename = tempfile.mkstemp()
340         self.tmpfile = os.fdopen(self.fd, 'r+')
341
342     def tearDown(self):
343         super(TestExec, self).tearDown()
344
345         try:
346             self.tmpfile.close()
347         finally:
348             os.remove(self.tmpfilename)
349
350     def eval_command(self, command):
351         gdb.execute('cy exec open(%r, "w").write(str(%s))' %
352                                                 (self.tmpfilename, command))
353         return self.tmpfile.read().strip()
354
355     def test_cython_exec(self):
356         self.break_and_run('os.path.join("foo", "bar")')
357
358         # test normal behaviour
359         self.assertEqual("[0]", self.eval_command('[a]'))
360
361         # test multiline code
362         result = gdb.execute(textwrap.dedent('''\
363             cy exec
364             pass
365
366             "nothing"
367             end
368             '''))
369         result = self.tmpfile.read().rstrip()
370         self.assertEqual('', result)
371
372     def test_python_exec(self):
373         self.break_and_run('os.path.join("foo", "bar")')
374         gdb.execute('cy step')
375
376         gdb.execute('cy exec some_random_var = 14')
377         self.assertEqual('14', self.eval_command('some_random_var'))
378
379 class TestClosure(DebugTestCase):
380
381     def break_and_run_func(self, funcname):
382         gdb.execute('cy break ' + funcname)
383         gdb.execute('cy run')
384
385     def test_inner(self):
386         self.break_and_run_func('inner')
387         self.assertEqual('', gdb.execute('cy locals', to_string=True))
388
389         # Allow the Cython-generated code to initialize the scope variable
390         gdb.execute('cy step')
391
392         self.assertEqual(str(self.read_var('a')), "'an object'")
393         print_result = gdb.execute('cy print a', to_string=True).strip()
394         self.assertEqual(print_result, "a = 'an object'")
395
396     def test_outer(self):
397         self.break_and_run_func('outer')
398         self.assertEqual('', gdb.execute('cy locals', to_string=True))
399
400         # Initialize scope with 'a' uninitialized
401         gdb.execute('cy step')
402         self.assertEqual('', gdb.execute('cy locals', to_string=True))
403
404         # Initialize 'a' to 1
405         gdb.execute('cy step')
406         print_result = gdb.execute('cy print a', to_string=True).strip()
407         self.assertEqual(print_result, "a = 'an object'")
408
409
410 _do_debug = os.environ.get('GDB_DEBUG')
411 if _do_debug:
412     _debug_file = open('/dev/tty', 'w')
413
414 def _debug(*messages):
415     if _do_debug:
416         messages = itertools.chain([sys._getframe(1).f_code.co_name, ':'],
417                                    messages)
418         _debug_file.write(' '.join(str(msg) for msg in messages) + '\n')
419
420
421 def run_unittest_in_module(modulename):
422     try:
423         gdb.lookup_type('PyModuleObject')
424     except RuntimeError:
425         msg = ("Unable to run tests, Python was not compiled with "
426                 "debugging information. Either compile python with "
427                 "-g or get a debug build (configure with --with-pydebug).")
428         warnings.warn(msg)
429         os._exit(0)
430     else:
431         m = __import__(modulename, fromlist=[''])
432         tests = inspect.getmembers(m, inspect.isclass)
433
434         # test_support.run_unittest(tests)
435
436         test_loader = unittest.TestLoader()
437         suite = unittest.TestSuite(
438             [test_loader.loadTestsFromTestCase(cls) for name, cls in tests])
439
440         result = unittest.TextTestRunner(verbosity=1).run(suite)
441         return result.wasSuccessful()
442
443 def runtests():
444     """
445     Run the libcython and libpython tests. Ensure that an appropriate status is
446     returned to the parent test process.
447     """
448     from Cython.Debugger.Tests import test_libpython_in_gdb
449
450     success_libcython = run_unittest_in_module(__name__)
451     success_libpython = run_unittest_in_module(test_libpython_in_gdb.__name__)
452
453     if not success_libcython or not success_libpython:
454         sys.exit(1)
455
456 def main(version, trace_code=False):
457     global inferior_python_version
458
459     inferior_python_version = version
460
461     if trace_code:
462         tracer = trace.Trace(count=False, trace=True, outfile=sys.stderr,
463                             ignoredirs=[sys.prefix, sys.exec_prefix])
464         tracer.runfunc(runtests)
465     else:
466         runtests()