1"""
2Tests that run inside GDB.
3
4Note: debug information is already imported by the file generated by
5Cython.Debugger.Cygdb.make_command_file()
6"""
7
8import os
9import re
10import sys
11import trace
12import inspect
13import warnings
14import unittest
15import textwrap
16import tempfile
17import functools
18import traceback
19import itertools
20from test import test_support
21
22import gdb
23
24from Cython.Debugger import libcython
25from Cython.Debugger import libpython
26from Cython.Debugger.Tests import TestLibCython as test_libcython
27
28# for some reason sys.argv is missing in gdb
29sys.argv = ['gdb']
30
31
32def 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
45class 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
53class 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
102class 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
142class 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
151class 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
178class 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
186class 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
196class 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
242class 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
259class 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
280class 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
302class 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
317class 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
325class 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
337class 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
382class 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
392class 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
407class 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')
439if _do_debug:
440    _debug_file = open('/dev/tty', 'w')
441
442def _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
449def 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
471def 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
484def 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