Merge from stable
[cython.git] / runtests.py
1 #!/usr/bin/python
2
3 import os, sys, re, shutil, unittest, doctest
4
5 WITH_CYTHON = True
6
7 from distutils.dist import Distribution
8 from distutils.core import Extension
9 from distutils.command.build_ext import build_ext
10 distutils_distro = Distribution()
11
12 TEST_DIRS = ['compile', 'errors', 'run', 'pyregr']
13 TEST_RUN_DIRS = ['run', 'pyregr']
14
15 # Lists external modules, and a matcher matching tests
16 # which should be excluded if the module is not present.
17 EXT_DEP_MODULES = {
18     'numpy' : re.compile('.*\.numpy_.*').match
19 }
20
21 INCLUDE_DIRS = [ d for d in os.getenv('INCLUDE', '').split(os.pathsep) if d ]
22 CFLAGS = os.getenv('CFLAGS', '').split()
23
24
25 class ErrorWriter(object):
26     match_error = re.compile('(warning:)?(?:.*:)?\s*([-0-9]+)\s*:\s*([-0-9]+)\s*:\s*(.*)').match
27     def __init__(self):
28         self.output = []
29         self.write = self.output.append
30
31     def _collect(self, collect_errors, collect_warnings):
32         s = ''.join(self.output)
33         result = []
34         for line in s.split('\n'):
35             match = self.match_error(line)
36             if match:
37                 is_warning, line, column, message = match.groups()
38                 if (is_warning and collect_warnings) or \
39                         (not is_warning and collect_errors):
40                     result.append( (int(line), int(column), message.strip()) )
41         result.sort()
42         return [ "%d:%d: %s" % values for values in result ]
43
44     def geterrors(self):
45         return self._collect(True, False)
46
47     def getwarnings(self):
48         return self._collect(False, True)
49
50     def getall(self):
51         return self._collect(True, True)
52
53 class TestBuilder(object):
54     def __init__(self, rootdir, workdir, selectors, exclude_selectors, annotate,
55                  cleanup_workdir, cleanup_sharedlibs, with_pyregr, cython_only,
56                  languages):
57         self.rootdir = rootdir
58         self.workdir = workdir
59         self.selectors = selectors
60         self.exclude_selectors = exclude_selectors
61         self.annotate = annotate
62         self.cleanup_workdir = cleanup_workdir
63         self.cleanup_sharedlibs = cleanup_sharedlibs
64         self.with_pyregr = with_pyregr
65         self.cython_only = cython_only
66         self.languages = languages
67
68     def build_suite(self):
69         suite = unittest.TestSuite()
70         filenames = os.listdir(self.rootdir)
71         filenames.sort()
72         for filename in filenames:
73             if not WITH_CYTHON and filename == "errors":
74                 # we won't get any errors without running Cython
75                 continue
76             path = os.path.join(self.rootdir, filename)
77             if os.path.isdir(path) and filename in TEST_DIRS:
78                 if filename == 'pyregr' and not self.with_pyregr:
79                     continue
80                 suite.addTest(
81                     self.handle_directory(path, filename))
82         return suite
83
84     def handle_directory(self, path, context):
85         workdir = os.path.join(self.workdir, context)
86         if not os.path.exists(workdir):
87             os.makedirs(workdir)
88
89         expect_errors = (context == 'errors')
90         suite = unittest.TestSuite()
91         filenames = os.listdir(path)
92         filenames.sort()
93         for filename in filenames:
94             if not (filename.endswith(".pyx") or filename.endswith(".py")):
95                 continue
96             if filename.startswith('.'): continue # certain emacs backup files
97             if context == 'pyregr' and not filename.startswith('test_'):
98                 continue
99             module = os.path.splitext(filename)[0]
100             fqmodule = "%s.%s" % (context, module)
101             if not [ 1 for match in self.selectors
102                      if match(fqmodule) ]:
103                 continue
104             if self.exclude_selectors:
105                 if [1 for match in self.exclude_selectors if match(fqmodule)]:
106                     continue
107             if context in TEST_RUN_DIRS:
108                 if module.startswith("test_"):
109                     test_class = CythonUnitTestCase
110                 else:
111                     test_class = CythonRunTestCase
112             else:
113                 test_class = CythonCompileTestCase
114             for test in self.build_tests(test_class, path, workdir,
115                                          module, expect_errors):
116                 suite.addTest(test)
117         return suite
118
119     def build_tests(self, test_class, path, workdir, module, expect_errors):
120         tests = [ self.build_test(test_class, path, workdir, module,
121                                   language, expect_errors)
122                   for language in self.languages ]
123         return tests
124
125     def build_test(self, test_class, path, workdir, module,
126                    language, expect_errors):
127         workdir = os.path.join(workdir, language)
128         if not os.path.exists(workdir):
129             os.makedirs(workdir)
130         return test_class(path, workdir, module,
131                           language=language,
132                           expect_errors=expect_errors,
133                           annotate=self.annotate,
134                           cleanup_workdir=self.cleanup_workdir,
135                           cleanup_sharedlibs=self.cleanup_sharedlibs,
136                           cython_only=self.cython_only)
137
138 class CythonCompileTestCase(unittest.TestCase):
139     def __init__(self, directory, workdir, module, language='c',
140                  expect_errors=False, annotate=False, cleanup_workdir=True,
141                  cleanup_sharedlibs=True, cython_only=False):
142         self.directory = directory
143         self.workdir = workdir
144         self.module = module
145         self.language = language
146         self.expect_errors = expect_errors
147         self.annotate = annotate
148         self.cleanup_workdir = cleanup_workdir
149         self.cleanup_sharedlibs = cleanup_sharedlibs
150         self.cython_only = cython_only
151         unittest.TestCase.__init__(self)
152
153     def shortDescription(self):
154         return "compiling (%s) %s" % (self.language, self.module)
155
156     def setUp(self):
157         if self.workdir not in sys.path:
158             sys.path.insert(0, self.workdir)
159
160     def tearDown(self):
161         try:
162             sys.path.remove(self.workdir)
163         except ValueError:
164             pass
165         try:
166             del sys.modules[self.module]
167         except KeyError:
168             pass
169         cleanup_c_files = WITH_CYTHON and self.cleanup_workdir
170         cleanup_lib_files = self.cleanup_sharedlibs
171         if os.path.exists(self.workdir):
172             for rmfile in os.listdir(self.workdir):
173                 if not cleanup_c_files:
174                     if rmfile[-2:] in (".c", ".h") or rmfile[-4:] == ".cpp":
175                         continue
176                 if not cleanup_lib_files and rmfile.endswith(".so") or rmfile.endswith(".dll"):
177                     continue
178                 if self.annotate and rmfile.endswith(".html"):
179                     continue
180                 try:
181                     rmfile = os.path.join(self.workdir, rmfile)
182                     if os.path.isdir(rmfile):
183                         shutil.rmtree(rmfile, ignore_errors=True)
184                     else:
185                         os.remove(rmfile)
186                 except IOError:
187                     pass
188         else:
189             os.makedirs(self.workdir)
190
191     def runTest(self):
192         self.runCompileTest()
193
194     def runCompileTest(self):
195         self.compile(self.directory, self.module, self.workdir,
196                      self.directory, self.expect_errors, self.annotate)
197
198     def find_module_source_file(self, source_file):
199         if not os.path.exists(source_file):
200             source_file = source_file[:-1]
201         return source_file
202
203     def build_target_filename(self, module_name):
204         target = '%s.%s' % (module_name, self.language)
205         return target
206
207     def split_source_and_output(self, directory, module, workdir):
208         source_file = os.path.join(directory, module) + '.pyx'
209         source_and_output = open(
210             self.find_module_source_file(source_file), 'rU')
211         out = open(os.path.join(workdir, module + '.pyx'), 'w')
212         for line in source_and_output:
213             last_line = line
214             if line.startswith("_ERRORS"):
215                 out.close()
216                 out = ErrorWriter()
217             else:
218                 out.write(line)
219         try:
220             geterrors = out.geterrors
221         except AttributeError:
222             return []
223         else:
224             return geterrors()
225
226     def run_cython(self, directory, module, targetdir, incdir, annotate):
227         include_dirs = INCLUDE_DIRS[:]
228         if incdir:
229             include_dirs.append(incdir)
230         source = self.find_module_source_file(
231             os.path.join(directory, module + '.pyx'))
232         target = os.path.join(targetdir, self.build_target_filename(module))
233         options = CompilationOptions(
234             pyrex_default_options,
235             include_path = include_dirs,
236             output_file = target,
237             annotate = annotate,
238             use_listing_file = False,
239             cplus = self.language == 'cpp',
240             generate_pxi = False)
241         cython_compile(source, options=options,
242                        full_module_name=module)
243
244     def run_distutils(self, module, workdir, incdir):
245         cwd = os.getcwd()
246         os.chdir(workdir)
247         try:
248             build_extension = build_ext(distutils_distro)
249             build_extension.include_dirs = INCLUDE_DIRS[:]
250             if incdir:
251                 build_extension.include_dirs.append(incdir)
252             build_extension.finalize_options()
253
254             extension = Extension(
255                 module,
256                 sources = [self.build_target_filename(module)],
257                 extra_compile_args = CFLAGS,
258                 )
259             build_extension.extensions = [extension]
260             build_extension.build_temp = workdir
261             build_extension.build_lib  = workdir
262             build_extension.run()
263         finally:
264             os.chdir(cwd)
265
266     def compile(self, directory, module, workdir, incdir,
267                 expect_errors, annotate):
268         expected_errors = errors = ()
269         if expect_errors:
270             expected_errors = self.split_source_and_output(
271                 directory, module, workdir)
272             directory = workdir
273
274         if WITH_CYTHON:
275             old_stderr = sys.stderr
276             try:
277                 sys.stderr = ErrorWriter()
278                 self.run_cython(directory, module, workdir, incdir, annotate)
279                 errors = sys.stderr.geterrors()
280             finally:
281                 sys.stderr = old_stderr
282
283         if errors or expected_errors:
284             for expected, error in zip(expected_errors, errors):
285                 self.assertEquals(expected, error)
286             if len(errors) < len(expected_errors):
287                 expected_error = expected_errors[len(errors)]
288                 self.assertEquals(expected_error, None)
289             elif len(errors) > len(expected_errors):
290                 unexpected_error = errors[len(expected_errors)]
291                 self.assertEquals(None, unexpected_error)
292         else:
293             if not self.cython_only:
294                 self.run_distutils(module, workdir, incdir)
295
296 class CythonRunTestCase(CythonCompileTestCase):
297     def shortDescription(self):
298         return "compiling (%s) and running %s" % (self.language, self.module)
299
300     def run(self, result=None):
301         if result is None:
302             result = self.defaultTestResult()
303         result.startTest(self)
304         try:
305             self.setUp()
306             self.runCompileTest()
307             if not self.cython_only:
308                 sys.stderr.write('running doctests in %s ...\n' % self.module)
309                 doctest.DocTestSuite(self.module).run(result)
310         except Exception:
311             result.addError(self, sys.exc_info())
312             result.stopTest(self)
313         try:
314             self.tearDown()
315         except Exception:
316             pass
317
318 class CythonUnitTestCase(CythonCompileTestCase):
319     def shortDescription(self):
320         return "compiling (%s) tests in %s" % (self.language, self.module)
321
322     def run(self, result=None):
323         if result is None:
324             result = self.defaultTestResult()
325         result.startTest(self)
326         try:
327             self.setUp()
328             self.runCompileTest()
329             sys.stderr.write('running tests in %s ...\n' % self.module)
330             unittest.defaultTestLoader.loadTestsFromName(self.module).run(result)
331         except Exception:
332             result.addError(self, sys.exc_info())
333             result.stopTest(self)
334         try:
335             self.tearDown()
336         except Exception:
337             pass
338
339 def collect_unittests(path, module_prefix, suite, selectors):
340     def file_matches(filename):
341         return filename.startswith("Test") and filename.endswith(".py")
342
343     def package_matches(dirname):
344         return dirname == "Tests"
345
346     loader = unittest.TestLoader()
347
348     skipped_dirs = []
349
350     for dirpath, dirnames, filenames in os.walk(path):
351         if dirpath != path and "__init__.py" not in filenames:
352             skipped_dirs.append(dirpath + os.path.sep)
353             continue
354         skip = False
355         for dir in skipped_dirs:
356             if dirpath.startswith(dir):
357                 skip = True
358         if skip:
359             continue
360         parentname = os.path.split(dirpath)[-1]
361         if package_matches(parentname):
362             for f in filenames:
363                 if file_matches(f):
364                     filepath = os.path.join(dirpath, f)[:-len(".py")]
365                     modulename = module_prefix + filepath[len(path)+1:].replace(os.path.sep, '.')
366                     if not [ 1 for match in selectors if match(modulename) ]:
367                         continue
368                     module = __import__(modulename)
369                     for x in modulename.split('.')[1:]:
370                         module = getattr(module, x)
371                     suite.addTests([loader.loadTestsFromModule(module)])
372
373 def collect_doctests(path, module_prefix, suite, selectors):
374     def package_matches(dirname):
375         return dirname not in ("Mac", "Distutils", "Plex")
376     def file_matches(filename):
377         return (filename.endswith(".py") and not ('~' in filename
378                 or '#' in filename or filename.startswith('.')))
379     import doctest, types
380     for dirpath, dirnames, filenames in os.walk(path):
381         parentname = os.path.split(dirpath)[-1]
382         if package_matches(parentname):
383             for f in filenames:
384                 if file_matches(f):
385                     if not f.endswith('.py'): continue
386                     filepath = os.path.join(dirpath, f)[:-len(".py")]
387                     modulename = module_prefix + filepath[len(path)+1:].replace(os.path.sep, '.')
388                     if not [ 1 for match in selectors if match(modulename) ]:
389                         continue
390                     module = __import__(modulename)
391                     for x in modulename.split('.')[1:]:
392                         module = getattr(module, x)
393                     if hasattr(module, "__doc__") or hasattr(module, "__test__"):
394                         try:
395                             suite.addTest(doctest.DocTestSuite(module))
396                         except ValueError: # no tests
397                             pass
398
399 class MissingDependencyExcluder:
400     def __init__(self, deps):
401         # deps: { module name : matcher func }
402         self.exclude_matchers = []
403         for mod, matcher in deps.items():
404             try:
405                 __import__(mod)
406             except ImportError:
407                 self.exclude_matchers.append(matcher)
408         self.tests_missing_deps = []
409     def __call__(self, testname):
410         for matcher in self.exclude_matchers:
411             if matcher(testname):
412                 self.tests_missing_deps.append(testname)
413                 return True
414         return False
415
416 if __name__ == '__main__':
417     from optparse import OptionParser
418     parser = OptionParser()
419     parser.add_option("--no-cleanup", dest="cleanup_workdir",
420                       action="store_false", default=True,
421                       help="do not delete the generated C files (allows passing --no-cython on next run)")
422     parser.add_option("--no-cleanup-sharedlibs", dest="cleanup_sharedlibs",
423                       action="store_false", default=True,
424                       help="do not delete the generated shared libary files (allows manual module experimentation)")
425     parser.add_option("--no-cython", dest="with_cython",
426                       action="store_false", default=True,
427                       help="do not run the Cython compiler, only the C compiler")
428     parser.add_option("--no-c", dest="use_c",
429                       action="store_false", default=True,
430                       help="do not test C compilation")
431     parser.add_option("--no-cpp", dest="use_cpp",
432                       action="store_false", default=True,
433                       help="do not test C++ compilation")
434     parser.add_option("--no-unit", dest="unittests",
435                       action="store_false", default=True,
436                       help="do not run the unit tests")
437     parser.add_option("--no-doctest", dest="doctests",
438                       action="store_false", default=True,
439                       help="do not run the doctests")
440     parser.add_option("--no-file", dest="filetests",
441                       action="store_false", default=True,
442                       help="do not run the file based tests")
443     parser.add_option("--no-pyregr", dest="pyregr",
444                       action="store_false", default=True,
445                       help="do not run the regression tests of CPython in tests/pyregr/")    
446     parser.add_option("--cython-only", dest="cython_only",
447                       action="store_true", default=False,
448                       help="only compile pyx to c, do not run C compiler or run the tests")
449     parser.add_option("--sys-pyregr", dest="system_pyregr",
450                       action="store_true", default=False,
451                       help="run the regression tests of the CPython installation")
452     parser.add_option("-x", "--exclude", dest="exclude",
453                       action="append", metavar="PATTERN",
454                       help="exclude tests matching the PATTERN")
455     parser.add_option("-C", "--coverage", dest="coverage",
456                       action="store_true", default=False,
457                       help="collect source coverage data for the Compiler")
458     parser.add_option("-A", "--annotate", dest="annotate_source",
459                       action="store_true", default=False,
460                       help="generate annotated HTML versions of the test source files")
461     parser.add_option("-v", "--verbose", dest="verbosity",
462                       action="count", default=0,
463                       help="display test progress, pass twice to print test names")
464
465     options, cmd_args = parser.parse_args()
466
467     if sys.version_info[0] >= 3:
468         # make sure we do not import (or run) Cython itself
469         options.doctests    = False
470         options.with_cython = False
471         options.unittests   = False
472         options.pyregr      = False
473
474     if options.coverage:
475         import coverage
476         coverage.erase()
477         coverage.start()
478
479     WITH_CYTHON = options.with_cython
480
481     if WITH_CYTHON:
482         from Cython.Compiler.Main import \
483             CompilationOptions, \
484             default_options as pyrex_default_options, \
485             compile as cython_compile
486         from Cython.Compiler import Errors
487         Errors.LEVEL = 0 # show all warnings
488
489     # RUN ALL TESTS!
490     ROOTDIR = os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]), 'tests')
491     WORKDIR = os.path.join(os.getcwd(), 'BUILD')
492     UNITTEST_MODULE = "Cython"
493     UNITTEST_ROOT = os.path.join(os.getcwd(), UNITTEST_MODULE)
494     if WITH_CYTHON:
495         if os.path.exists(WORKDIR):
496             shutil.rmtree(WORKDIR, ignore_errors=True)
497     if not os.path.exists(WORKDIR):
498         os.makedirs(WORKDIR)
499
500     if WITH_CYTHON:
501         from Cython.Compiler.Version import version
502         sys.stderr.write("Running tests against Cython %s\n" % version)
503     else:
504         sys.stderr.write("Running tests without Cython.\n")
505     sys.stderr.write("Python %s\n" % sys.version)
506     sys.stderr.write("\n")
507
508     import re
509     selectors = [ re.compile(r, re.I|re.U).search for r in cmd_args ]
510     if not selectors:
511         selectors = [ lambda x:True ]
512
513     # Chech which external modules are not present and exclude tests
514     # which depends on them (by prefix)
515
516     missing_dep_excluder = MissingDependencyExcluder(EXT_DEP_MODULES) 
517     exclude_selectors = [missing_dep_excluder] # want to pring msg at exit
518
519     if options.exclude:
520         exclude_selectors += [ re.compile(r, re.I|re.U).search for r in options.exclude ]
521
522     languages = []
523     if options.use_c:
524         languages.append('c')
525     if options.use_cpp:
526         languages.append('cpp')
527
528     test_suite = unittest.TestSuite()
529
530     if options.unittests:
531         collect_unittests(UNITTEST_ROOT, UNITTEST_MODULE + ".", test_suite, selectors)
532
533     if options.doctests:
534         collect_doctests(UNITTEST_ROOT, UNITTEST_MODULE + ".", test_suite, selectors)
535
536     if options.filetests and languages:
537         filetests = TestBuilder(ROOTDIR, WORKDIR, selectors, exclude_selectors,
538                                 options.annotate_source, options.cleanup_workdir,
539                                 options.cleanup_sharedlibs, options.pyregr,
540                                 options.cython_only, languages)
541         test_suite.addTest(filetests.build_suite())
542
543     if options.system_pyregr and languages:
544         filetests = TestBuilder(ROOTDIR, WORKDIR, selectors, exclude_selectors,
545                                 options.annotate_source, options.cleanup_workdir,
546                                 options.cleanup_sharedlibs, True,
547                                 options.cython_only, languages)
548         test_suite.addTest(
549             filetests.handle_directory(
550                 os.path.join(sys.prefix, 'lib', 'python'+sys.version[:3], 'test'),
551                 'pyregr'))
552
553     unittest.TextTestRunner(verbosity=options.verbosity).run(test_suite)
554
555     if options.coverage:
556         coverage.stop()
557         ignored_modules = ('Options', 'Version', 'DebugFlags')
558         modules = [ module for name, module in sys.modules.items()
559                     if module is not None and
560                     name.startswith('Cython.Compiler.') and 
561                     name[len('Cython.Compiler.'):] not in ignored_modules ]
562         coverage.report(modules, show_missing=0)
563
564     if missing_dep_excluder.tests_missing_deps:
565         sys.stderr.write("Following tests excluded because of missing dependencies on your system:\n")
566         for test in missing_dep_excluder.tests_missing_deps:
567             sys.stderr.write("   %s\n" % test)