Fix Python 3 testrunner and give a warning for Python 2 builds without debug symbols
[cython.git] / Cython / Debugger / Tests / TestLibCython.py
1 from __future__ import with_statement
2
3 import os
4 import re
5 import sys
6 import uuid
7 import shutil
8 import warnings
9 import textwrap
10 import unittest
11 import tempfile
12 import subprocess
13 import distutils.core
14 from distutils import sysconfig
15 from distutils import ccompiler
16
17 import runtests
18 import Cython.Distutils.extension
19 import Cython.Distutils.build_ext
20 from Cython.Debugger import Cygdb as cygdb
21
22 root = os.path.dirname(os.path.abspath(__file__))
23 codefile = os.path.join(root, 'codefile')
24 cfuncs_file = os.path.join(root, 'cfuncs.c')
25 with open(codefile) as f:
26     source_to_lineno = dict((line.strip(), i + 1) for i, line in enumerate(f))
27
28 # Cython.Distutils.__init__ imports build_ext from build_ext which means we
29 # can't access the module anymore. Get it from sys.modules instead.
30 build_ext = sys.modules['Cython.Distutils.build_ext']
31
32 class DebuggerTestCase(unittest.TestCase):
33
34     def setUp(self):
35         """
36         Run gdb and have cygdb import the debug information from the code
37         defined in TestParseTreeTransforms's setUp method
38         """
39         self.tempdir = tempfile.mkdtemp()
40         self.destfile = os.path.join(self.tempdir, 'codefile.pyx')
41         self.debug_dest = os.path.join(self.tempdir,
42                                       'cython_debug',
43                                       'cython_debug_info_codefile')
44         self.cfuncs_destfile = os.path.join(self.tempdir, 'cfuncs')
45
46         self.cwd = os.getcwd()
47         os.chdir(self.tempdir)
48
49         shutil.copy(codefile, self.destfile)
50         shutil.copy(cfuncs_file, self.cfuncs_destfile + '.c')
51
52         compiler = ccompiler.new_compiler()
53         compiler.compile(['cfuncs.c'], debug=True, extra_postargs=['-fPIC'])
54
55         opts = dict(
56             test_directory=self.tempdir,
57             module='codefile',
58         )
59
60         optimization_disabler = build_ext.Optimization()
61         optimization_disabler.disable_optimization()
62
63         cython_compile_testcase = runtests.CythonCompileTestCase(
64             workdir=self.tempdir,
65             # we clean up everything (not only compiled files)
66             cleanup_workdir=False,
67             **opts
68         )
69
70         cython_compile_testcase.run_cython(
71             targetdir=self.tempdir,
72             incdir=None,
73             annotate=False,
74             extra_compile_options={
75                 'gdb_debug':True,
76                 'output_dir':self.tempdir,
77             },
78             **opts
79         )
80
81         cython_compile_testcase.run_distutils(
82             incdir=None,
83             workdir=self.tempdir,
84             extra_extension_args={'extra_objects':['cfuncs.o']},
85             **opts
86         )
87
88         optimization_disabler.restore_state()
89
90         # ext = Cython.Distutils.extension.Extension(
91             # 'codefile',
92             # ['codefile.pyx'],
93             # pyrex_gdb=True,
94             # extra_objects=['cfuncs.o'])
95         #
96         # distutils.core.setup(
97             # script_args=['build_ext', '--inplace'],
98             # ext_modules=[ext],
99             # cmdclass=dict(build_ext=Cython.Distutils.build_ext)
100         # )
101
102     def tearDown(self):
103         os.chdir(self.cwd)
104         shutil.rmtree(self.tempdir)
105
106
107 class GdbDebuggerTestCase(DebuggerTestCase):
108
109     def setUp(self):
110         super(GdbDebuggerTestCase, self).setUp()
111
112         prefix_code = textwrap.dedent('''\
113             python
114
115             import os
116             import sys
117             import traceback
118
119             def excepthook(type, value, tb):
120                 traceback.print_exception(type, value, tb)
121                 os._exit(1)
122
123             sys.excepthook = excepthook
124
125             # Have tracebacks end up on sys.stderr (gdb replaces sys.stderr
126             # with an object that calls gdb.write())
127             sys.stderr = sys.__stderr__
128
129             end
130             ''')
131
132         code = textwrap.dedent('''\
133             python
134
135             from Cython.Debugger.Tests import test_libcython_in_gdb
136             test_libcython_in_gdb.main(version=%r)
137
138             end
139             ''' % (sys.version_info[:2],))
140
141         self.gdb_command_file = cygdb.make_command_file(self.tempdir,
142                                                         prefix_code)
143
144         with open(self.gdb_command_file, 'a') as f:
145             f.write(code)
146
147         args = ['gdb', '-batch', '-x', self.gdb_command_file, '-n', '--args',
148                 sys.executable, '-c', 'import codefile']
149
150         paths = []
151         path = os.environ.get('PYTHONPATH')
152         if path:
153             paths.append(path)
154         paths.append(os.path.dirname(os.path.dirname(
155             os.path.abspath(Cython.__file__))))
156         env = dict(os.environ, PYTHONPATH=os.pathsep.join(paths))
157
158         try:
159             p = subprocess.Popen(['gdb', '-v'], stdout=subprocess.PIPE)
160             have_gdb = True
161         except OSError:
162             # gdb was not installed
163             have_gdb = False
164         else:
165             gdb_version = p.stdout.read().decode('ascii')
166             p.wait()
167             p.stdout.close()
168
169         if have_gdb:
170             # Based on Lib/test/test_gdb.py
171             regex = "^GNU gdb [^\d]*(\d+)\.(\d+)"
172             gdb_version_number = re.search(regex, gdb_version).groups()
173
174         # Be Python 3 compatible
175         if not have_gdb or list(map(int, gdb_version_number)) < [7, 2]:
176             self.p = None
177             warnings.warn('Skipping gdb tests, need gdb >= 7.2')
178         else:
179             self.p = subprocess.Popen(
180                 args,
181                 stdout=open(os.devnull, 'w'),
182                 stderr=subprocess.PIPE,
183                 env=env)
184
185     def tearDown(self):
186         super(GdbDebuggerTestCase, self).tearDown()
187         if self.p:
188             self.p.stderr.close()
189             self.p.wait()
190         os.remove(self.gdb_command_file)
191
192
193 class TestAll(GdbDebuggerTestCase):
194
195     def test_all(self):
196         if self.p is None:
197             return
198
199         out, err = self.p.communicate()
200         err = err.decode('UTF-8')
201         border = '*' * 30
202         start = '%s   v INSIDE GDB v   %s' % (border, border)
203         end   = '%s   ^ INSIDE GDB ^   %s' % (border, border)
204         errmsg = '\n%s\n%s%s' % (start, err, end)
205         self.assertEquals(0, self.p.wait(), errmsg)
206         sys.stderr.write(err)
207
208 if __name__ == '__main__':
209     unittest.main()