require options '-v' or '-vv' for status output in test runner
[cython.git] / runtests.py
1 #!/usr/bin/python
2
3 import os, sys, re, shutil, unittest, doctest
4
5 WITH_CYTHON = True
6 CLEANUP_WORKDIR = True
7
8 from distutils.dist import Distribution
9 from distutils.core import Extension
10 from distutils.command.build_ext import build_ext
11 distutils_distro = Distribution()
12
13 TEST_DIRS = ['compile', 'errors', 'run']
14 TEST_RUN_DIRS = ['run']
15
16 INCLUDE_DIRS = [ d for d in os.getenv('INCLUDE', '').split(os.pathsep) if d ]
17 CFLAGS = os.getenv('CFLAGS', '').split()
18
19
20 class ErrorWriter(object):
21     match_error = re.compile('(?:.*:)?([-0-9]+):([-0-9]+):(.*)').match
22     def __init__(self):
23         self.output = []
24         self.write = self.output.append
25
26     def geterrors(self):
27         s = ''.join(self.output)
28         errors = []
29         for line in s.split('\n'):
30             match = self.match_error(line)
31             if match:
32                 line, column, message = match.groups()
33                 errors.append( "%d:%d:%s" % (int(line), int(column), message.strip()) )
34         return errors
35
36 class TestBuilder(object):
37     def __init__(self, rootdir, workdir, selectors, annotate):
38         self.rootdir = rootdir
39         self.workdir = workdir
40         self.selectors = selectors
41         self.annotate = annotate
42
43     def build_suite(self):
44         suite = unittest.TestSuite()
45         filenames = os.listdir(self.rootdir)
46         filenames.sort()
47         for filename in filenames:
48             if not WITH_CYTHON and filename == "errors":
49                 # we won't get any errors without running Cython
50                 continue
51             path = os.path.join(self.rootdir, filename)
52             if os.path.isdir(path) and filename in TEST_DIRS:
53                 suite.addTest(
54                     self.handle_directory(path, filename))
55         return suite
56
57     def handle_directory(self, path, context):
58         workdir = os.path.join(self.workdir, context)
59         if not os.path.exists(workdir):
60             os.makedirs(workdir)
61         if workdir not in sys.path:
62             sys.path.insert(0, workdir)
63
64         expect_errors = (context == 'errors')
65         suite = unittest.TestSuite()
66         filenames = os.listdir(path)
67         filenames.sort()
68         for filename in filenames:
69             if not filename.endswith(".pyx"):
70                 continue
71             module = filename[:-4]
72             fqmodule = "%s.%s" % (context, module)
73             if not [ 1 for match in self.selectors
74                      if match(fqmodule) ]:
75                 continue
76             if context in TEST_RUN_DIRS:
77                 test = CythonRunTestCase(
78                     path, workdir, module, self.annotate)
79             else:
80                 test = CythonCompileTestCase(
81                     path, workdir, module, expect_errors, self.annotate)
82             suite.addTest(test)
83         return suite
84
85 class CythonCompileTestCase(unittest.TestCase):
86     def __init__(self, directory, workdir, module,
87                  expect_errors=False, annotate=False):
88         self.directory = directory
89         self.workdir = workdir
90         self.module = module
91         self.expect_errors = expect_errors
92         self.annotate = annotate
93         unittest.TestCase.__init__(self)
94
95     def shortDescription(self):
96         return "compiling " + self.module
97
98     def tearDown(self):
99         cleanup_c_files = WITH_CYTHON and CLEANUP_WORKDIR
100         if os.path.exists(self.workdir):
101             for rmfile in os.listdir(self.workdir):
102                 if not cleanup_c_files and rmfile[-2:] in (".c", ".h"):
103                     continue
104                 if self.annotate and rmfile.endswith(".html"):
105                     continue
106                 try:
107                     rmfile = os.path.join(self.workdir, rmfile)
108                     if os.path.isdir(rmfile):
109                         shutil.rmtree(rmfile, ignore_errors=True)
110                     else:
111                         os.remove(rmfile)
112                 except IOError:
113                     pass
114         else:
115             os.makedirs(self.workdir)
116
117     def runTest(self):
118         self.compile(self.directory, self.module, self.workdir,
119                      self.directory, self.expect_errors, self.annotate)
120
121     def split_source_and_output(self, directory, module, workdir):
122         source_and_output = open(os.path.join(directory, module + '.pyx'), 'rU')
123         out = open(os.path.join(workdir, module + '.pyx'), 'w')
124         for line in source_and_output:
125             last_line = line
126             if line.startswith("_ERRORS"):
127                 out.close()
128                 out = ErrorWriter()
129             else:
130                 out.write(line)
131         try:
132             geterrors = out.geterrors
133         except AttributeError:
134             return []
135         else:
136             return geterrors()
137
138     def run_cython(self, directory, module, targetdir, incdir, annotate):
139         include_dirs = INCLUDE_DIRS[:]
140         if incdir:
141             include_dirs.append(incdir)
142         source = os.path.join(directory, module + '.pyx')
143         target = os.path.join(targetdir, module + '.c')
144         options = CompilationOptions(
145             pyrex_default_options,
146             include_path = include_dirs,
147             output_file = target,
148             annotate = annotate,
149             use_listing_file = False, cplus = False, generate_pxi = False)
150         cython_compile(source, options=options,
151                        full_module_name=module)
152
153     def run_distutils(self, module, workdir, incdir):
154         cwd = os.getcwd()
155         os.chdir(workdir)
156         try:
157             build_extension = build_ext(distutils_distro)
158             build_extension.include_dirs = INCLUDE_DIRS[:]
159             if incdir:
160                 build_extension.include_dirs.append(incdir)
161             build_extension.finalize_options()
162
163             extension = Extension(
164                 module,
165                 sources = [module + '.c'],
166                 extra_compile_args = CFLAGS,
167                 )
168             build_extension.extensions = [extension]
169             build_extension.build_temp = workdir
170             build_extension.build_lib  = workdir
171             build_extension.run()
172         finally:
173             os.chdir(cwd)
174
175     def compile(self, directory, module, workdir, incdir,
176                 expect_errors, annotate):
177         expected_errors = errors = ()
178         if expect_errors:
179             expected_errors = self.split_source_and_output(
180                 directory, module, workdir)
181             directory = workdir
182
183         if WITH_CYTHON:
184             old_stderr = sys.stderr
185             try:
186                 sys.stderr = ErrorWriter()
187                 self.run_cython(directory, module, workdir, incdir, annotate)
188                 errors = sys.stderr.geterrors()
189             finally:
190                 sys.stderr = old_stderr
191
192         if errors or expected_errors:
193             for expected, error in zip(expected_errors, errors):
194                 self.assertEquals(expected, error)
195             if len(errors) < len(expected_errors):
196                 expected_error = expected_errors[len(errors)]
197                 self.assertEquals(expected_error, None)
198             elif len(errors) > len(expected_errors):
199                 unexpected_error = errors[len(expected_errors)]
200                 self.assertEquals(None, unexpected_error)
201         else:
202             self.run_distutils(module, workdir, incdir)
203
204 class CythonRunTestCase(CythonCompileTestCase):
205     def shortDescription(self):
206         return "compiling and running " + self.module
207
208     def run(self, result=None):
209         if result is None:
210             result = self.defaultTestResult()
211         result.startTest(self)
212         try:
213             self.runTest()
214             doctest.DocTestSuite(self.module).run(result)
215         except Exception:
216             result.addError(self, sys.exc_info())
217             result.stopTest(self)
218         try:
219             self.tearDown()
220         except Exception:
221             pass
222
223 if __name__ == '__main__':
224     try:
225         sys.argv.remove("--no-cython")
226     except ValueError:
227         WITH_CYTHON = True
228     else:
229         WITH_CYTHON = False
230
231     if WITH_CYTHON:
232         from Cython.Compiler.Main import \
233             CompilationOptions, \
234             default_options as pyrex_default_options, \
235             compile as cython_compile
236
237     from distutils.dist import Distribution
238     from distutils.core import Extension
239     from distutils.command.build_ext import build_ext
240     distutils_distro = Distribution()
241
242     # RUN ALL TESTS!
243     ROOTDIR = os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]), 'tests')
244     WORKDIR = os.path.join(os.getcwd(), 'BUILD')
245     if WITH_CYTHON:
246         if os.path.exists(WORKDIR):
247             shutil.rmtree(WORKDIR, ignore_errors=True)
248     if not os.path.exists(WORKDIR):
249         os.makedirs(WORKDIR)
250
251     if WITH_CYTHON:
252         from Cython.Compiler.Version import version
253         from Cython.Compiler.Main import \
254             CompilationOptions, \
255             default_options as pyrex_default_options, \
256             compile as cython_compile
257         print("Running tests against Cython %s" % version)
258     else:
259         print("Running tests without Cython.")
260     print("Python %s" % sys.version)
261     print("")
262
263     try:
264         sys.argv.remove("-C")
265     except ValueError:
266         coverage = None
267     else:
268         import coverage
269         coverage.erase()
270
271     try:
272         sys.argv.remove("--no-cleanup")
273     except ValueError:
274         CLEANUP_WORKDIR = True
275     else:
276         CLEANUP_WORKDIR = False
277
278     try:
279         sys.argv.remove("-a")
280     except ValueError:
281         annotate_source = False
282     else:
283         annotate_source = True
284
285     try:
286         sys.argv.remove("-vv")
287     except ValueError:
288         try:
289             sys.argv.remove("-v")
290         except ValueError:
291             verbosity = 0
292         else:
293             verbosity = 1
294     else:
295         verbosity = 2
296
297     import re
298     selectors = [ re.compile(r, re.I|re.U).search for r in sys.argv[1:] ]
299     if not selectors:
300         selectors = [ lambda x:True ]
301
302     tests = TestBuilder(ROOTDIR, WORKDIR, selectors, annotate_source)
303     test_suite = tests.build_suite()
304
305     if coverage is not None:
306         coverage.start()
307
308     unittest.TextTestRunner(verbosity=verbosity).run(test_suite)
309
310     if coverage is not None:
311         coverage.stop()
312         ignored_modules = ('Options', 'Version', 'DebugFlags')
313         modules = [ module for name, module in sys.modules.items()
314                     if module is not None and
315                     name.startswith('Cython.Compiler.') and 
316                     name[len('Cython.Compiler.'):] not in ignored_modules ]
317         coverage.report(modules, show_missing=0)