Fix leak in try-break.
[cython.git] / runtests.py
index 5706201e7229de876f4421e73543017aabcc7365..8e8925e1deefda6157df2d9afd19727ee401eda7 100644 (file)
@@ -6,15 +6,34 @@ WITH_CYTHON = True
 
 from distutils.dist import Distribution
 from distutils.core import Extension
-from distutils.command.build_ext import build_ext
+from distutils.command.build_ext import build_ext as _build_ext
 distutils_distro = Distribution()
 
-TEST_DIRS = ['compile', 'errors', 'run']
-TEST_RUN_DIRS = ['run']
+TEST_DIRS = ['compile', 'errors', 'run', 'pyregr']
+TEST_RUN_DIRS = ['run', 'pyregr', 'bugs']
+
+# Lists external modules, and a matcher matching tests
+# which should be excluded if the module is not present.
+EXT_DEP_MODULES = {
+    'numpy' : re.compile('.*\.numpy_.*').match
+}
+
+VER_DEP_MODULES = {
+# such as:
+#    (2,4) : lambda x: x in ['run.set']
+}
 
 INCLUDE_DIRS = [ d for d in os.getenv('INCLUDE', '').split(os.pathsep) if d ]
 CFLAGS = os.getenv('CFLAGS', '').split()
 
+class build_ext(_build_ext):
+    def build_extension(self, ext):
+        if ext.language == 'c++':
+            try:
+                self.compiler.compiler_so.remove('-Wstrict-prototypes')
+            except Exception:
+                pass
+        _build_ext.build_extension(self, ext)
 
 class ErrorWriter(object):
     match_error = re.compile('(warning:)?(?:.*:)?\s*([-0-9]+)\s*:\s*([-0-9]+)\s*:\s*(.*)').match
@@ -45,15 +64,26 @@ class ErrorWriter(object):
         return self._collect(True, True)
 
 class TestBuilder(object):
-    def __init__(self, rootdir, workdir, selectors, annotate, cleanup_workdir):
+    def __init__(self, rootdir, workdir, selectors, exclude_selectors, annotate,
+                 cleanup_workdir, cleanup_sharedlibs, with_pyregr, cython_only,
+                 languages, test_bugs):
         self.rootdir = rootdir
         self.workdir = workdir
         self.selectors = selectors
+        self.exclude_selectors = exclude_selectors
         self.annotate = annotate
         self.cleanup_workdir = cleanup_workdir
+        self.cleanup_sharedlibs = cleanup_sharedlibs
+        self.with_pyregr = with_pyregr
+        self.cython_only = cython_only
+        self.languages = languages
+        self.test_bugs = test_bugs
 
     def build_suite(self):
         suite = unittest.TestSuite()
+        test_dirs = TEST_DIRS
+        if self.test_bugs and 'bugs' not in test_dirs:
+            test_dirs.append('bugs')
         filenames = os.listdir(self.rootdir)
         filenames.sort()
         for filename in filenames:
@@ -61,7 +91,9 @@ class TestBuilder(object):
                 # we won't get any errors without running Cython
                 continue
             path = os.path.join(self.rootdir, filename)
-            if os.path.isdir(path) and filename in TEST_DIRS:
+            if os.path.isdir(path) and filename in test_dirs:
+                if filename == 'pyregr' and not self.with_pyregr:
+                    continue
                 suite.addTest(
                     self.handle_directory(path, filename))
         return suite
@@ -70,54 +102,99 @@ class TestBuilder(object):
         workdir = os.path.join(self.workdir, context)
         if not os.path.exists(workdir):
             os.makedirs(workdir)
-        if workdir not in sys.path:
-            sys.path.insert(0, workdir)
 
         expect_errors = (context == 'errors')
         suite = unittest.TestSuite()
         filenames = os.listdir(path)
         filenames.sort()
         for filename in filenames:
-            if not filename.endswith(".pyx"):
+            if not (filename.endswith(".pyx") or filename.endswith(".py")):
+                continue
+            if filename.startswith('.'): continue # certain emacs backup files
+            if context == 'pyregr' and not filename.startswith('test_'):
                 continue
-            module = filename[:-4]
+            module = os.path.splitext(filename)[0]
             fqmodule = "%s.%s" % (context, module)
             if not [ 1 for match in self.selectors
                      if match(fqmodule) ]:
                 continue
+            if self.exclude_selectors:
+                if [1 for match in self.exclude_selectors if match(fqmodule)]:
+                    continue
             if context in TEST_RUN_DIRS:
-                test = CythonRunTestCase(
-                    path, workdir, module,
-                    annotate=self.annotate,
-                    cleanup_workdir=self.cleanup_workdir)
+                if module.startswith("test_"):
+                    test_class = CythonUnitTestCase
+                else:
+                    test_class = CythonRunTestCase
             else:
-                test = CythonCompileTestCase(
-                    path, workdir, module,
-                    expect_errors=expect_errors,
-                    annotate=self.annotate,
-                    cleanup_workdir=self.cleanup_workdir)
-            suite.addTest(test)
+                test_class = CythonCompileTestCase
+            for test in self.build_tests(test_class, path, workdir,
+                                         module, expect_errors):
+                suite.addTest(test)
         return suite
 
+    def build_tests(self, test_class, path, workdir, module, expect_errors):
+        if expect_errors:
+            languages = self.languages[:1]
+        else:
+            languages = self.languages
+        tests = [ self.build_test(test_class, path, workdir, module,
+                                  language, expect_errors)
+                  for language in languages ]
+        return tests
+
+    def build_test(self, test_class, path, workdir, module,
+                   language, expect_errors):
+        workdir = os.path.join(workdir, language)
+        if not os.path.exists(workdir):
+            os.makedirs(workdir)
+        return test_class(path, workdir, module,
+                          language=language,
+                          expect_errors=expect_errors,
+                          annotate=self.annotate,
+                          cleanup_workdir=self.cleanup_workdir,
+                          cleanup_sharedlibs=self.cleanup_sharedlibs,
+                          cython_only=self.cython_only)
+
 class CythonCompileTestCase(unittest.TestCase):
-    def __init__(self, directory, workdir, module,
-                 expect_errors=False, annotate=False, cleanup_workdir=True):
+    def __init__(self, directory, workdir, module, language='c',
+                 expect_errors=False, annotate=False, cleanup_workdir=True,
+                 cleanup_sharedlibs=True, cython_only=False):
         self.directory = directory
         self.workdir = workdir
         self.module = module
+        self.language = language
         self.expect_errors = expect_errors
         self.annotate = annotate
         self.cleanup_workdir = cleanup_workdir
+        self.cleanup_sharedlibs = cleanup_sharedlibs
+        self.cython_only = cython_only
         unittest.TestCase.__init__(self)
 
     def shortDescription(self):
-        return "compiling " + self.module
+        return "compiling (%s) %s" % (self.language, self.module)
+
+    def setUp(self):
+        if self.workdir not in sys.path:
+            sys.path.insert(0, self.workdir)
 
     def tearDown(self):
+        try:
+            sys.path.remove(self.workdir)
+        except ValueError:
+            pass
+        try:
+            del sys.modules[self.module]
+        except KeyError:
+            pass
         cleanup_c_files = WITH_CYTHON and self.cleanup_workdir
+        cleanup_lib_files = self.cleanup_sharedlibs
         if os.path.exists(self.workdir):
             for rmfile in os.listdir(self.workdir):
-                if not cleanup_c_files and rmfile[-2:] in (".c", ".h"):
+                if not cleanup_c_files:
+                    if rmfile[-2:] in (".c", ".h") or rmfile[-4:] == ".cpp":
+                        continue
+                if not cleanup_lib_files and rmfile.endswith(".so") or rmfile.endswith(".dll"):
                     continue
                 if self.annotate and rmfile.endswith(".html"):
                     continue
@@ -133,11 +210,25 @@ class CythonCompileTestCase(unittest.TestCase):
             os.makedirs(self.workdir)
 
     def runTest(self):
+        self.runCompileTest()
+
+    def runCompileTest(self):
         self.compile(self.directory, self.module, self.workdir,
                      self.directory, self.expect_errors, self.annotate)
 
+    def find_module_source_file(self, source_file):
+        if not os.path.exists(source_file):
+            source_file = source_file[:-1]
+        return source_file
+
+    def build_target_filename(self, module_name):
+        target = '%s.%s' % (module_name, self.language)
+        return target
+
     def split_source_and_output(self, directory, module, workdir):
-        source_and_output = open(os.path.join(directory, module + '.pyx'), 'rU')
+        source_file = os.path.join(directory, module) + '.pyx'
+        source_and_output = open(
+            self.find_module_source_file(source_file), 'rU')
         out = open(os.path.join(workdir, module + '.pyx'), 'w')
         for line in source_and_output:
             last_line = line
@@ -157,14 +248,17 @@ class CythonCompileTestCase(unittest.TestCase):
         include_dirs = INCLUDE_DIRS[:]
         if incdir:
             include_dirs.append(incdir)
-        source = os.path.join(directory, module + '.pyx')
-        target = os.path.join(targetdir, module + '.c')
+        source = self.find_module_source_file(
+            os.path.join(directory, module + '.pyx'))
+        target = os.path.join(targetdir, self.build_target_filename(module))
         options = CompilationOptions(
             pyrex_default_options,
             include_path = include_dirs,
             output_file = target,
             annotate = annotate,
-            use_listing_file = False, cplus = False, generate_pxi = False)
+            use_listing_file = False,
+            cplus = self.language == 'cpp',
+            generate_pxi = False)
         cython_compile(source, options=options,
                        full_module_name=module)
 
@@ -177,12 +271,13 @@ class CythonCompileTestCase(unittest.TestCase):
             if incdir:
                 build_extension.include_dirs.append(incdir)
             build_extension.finalize_options()
-
             extension = Extension(
                 module,
-                sources = [module + '.c'],
+                sources = [self.build_target_filename(module)],
                 extra_compile_args = CFLAGS,
                 )
+            if self.language == 'cpp':
+                extension.language = 'c++'
             build_extension.extensions = [extension]
             build_extension.build_temp = workdir
             build_extension.build_lib  = workdir
@@ -217,19 +312,22 @@ class CythonCompileTestCase(unittest.TestCase):
                 unexpected_error = errors[len(expected_errors)]
                 self.assertEquals(None, unexpected_error)
         else:
-            self.run_distutils(module, workdir, incdir)
+            if not self.cython_only:
+                self.run_distutils(module, workdir, incdir)
 
 class CythonRunTestCase(CythonCompileTestCase):
     def shortDescription(self):
-        return "compiling and running " + self.module
+        return "compiling (%s) and running %s" % (self.language, self.module)
 
     def run(self, result=None):
         if result is None:
             result = self.defaultTestResult()
         result.startTest(self)
         try:
-            self.runTest()
-            doctest.DocTestSuite(self.module).run(result)
+            self.setUp()
+            self.runCompileTest()
+            if not self.cython_only:
+                doctest.DocTestSuite(self.module).run(result)
         except Exception:
             result.addError(self, sys.exc_info())
             result.stopTest(self)
@@ -238,7 +336,27 @@ class CythonRunTestCase(CythonCompileTestCase):
         except Exception:
             pass
 
-def collect_unittests(path, suite, selectors):
+class CythonUnitTestCase(CythonCompileTestCase):
+    def shortDescription(self):
+        return "compiling (%s) tests in %s" % (self.language, self.module)
+
+    def run(self, result=None):
+        if result is None:
+            result = self.defaultTestResult()
+        result.startTest(self)
+        try:
+            self.setUp()
+            self.runCompileTest()
+            unittest.defaultTestLoader.loadTestsFromName(self.module).run(result)
+        except Exception:
+            result.addError(self, sys.exc_info())
+            result.stopTest(self)
+        try:
+            self.tearDown()
+        except Exception:
+            pass
+
+def collect_unittests(path, module_prefix, suite, selectors):
     def file_matches(filename):
         return filename.startswith("Test") and filename.endswith(".py")
 
@@ -247,19 +365,89 @@ def collect_unittests(path, suite, selectors):
 
     loader = unittest.TestLoader()
 
+    skipped_dirs = []
+
     for dirpath, dirnames, filenames in os.walk(path):
+        if dirpath != path and "__init__.py" not in filenames:
+            skipped_dirs.append(dirpath + os.path.sep)
+            continue
+        skip = False
+        for dir in skipped_dirs:
+            if dirpath.startswith(dir):
+                skip = True
+        if skip:
+            continue
         parentname = os.path.split(dirpath)[-1]
         if package_matches(parentname):
             for f in filenames:
                 if file_matches(f):
                     filepath = os.path.join(dirpath, f)[:-len(".py")]
-                    modulename = filepath[len(path)+1:].replace(os.path.sep, '.')
+                    modulename = module_prefix + filepath[len(path)+1:].replace(os.path.sep, '.')
                     if not [ 1 for match in selectors if match(modulename) ]:
                         continue
                     module = __import__(modulename)
                     for x in modulename.split('.')[1:]:
                         module = getattr(module, x)
-                    suite.addTests(loader.loadTestsFromModule(module))
+                    suite.addTests([loader.loadTestsFromModule(module)])
+
+def collect_doctests(path, module_prefix, suite, selectors):
+    def package_matches(dirname):
+        return dirname not in ("Mac", "Distutils", "Plex")
+    def file_matches(filename):
+        return (filename.endswith(".py") and not ('~' in filename
+                or '#' in filename or filename.startswith('.')))
+    import doctest, types
+    for dirpath, dirnames, filenames in os.walk(path):
+        parentname = os.path.split(dirpath)[-1]
+        if package_matches(parentname):
+            for f in filenames:
+                if file_matches(f):
+                    if not f.endswith('.py'): continue
+                    filepath = os.path.join(dirpath, f)[:-len(".py")]
+                    modulename = module_prefix + filepath[len(path)+1:].replace(os.path.sep, '.')
+                    if not [ 1 for match in selectors if match(modulename) ]:
+                        continue
+                    module = __import__(modulename)
+                    for x in modulename.split('.')[1:]:
+                        module = getattr(module, x)
+                    if hasattr(module, "__doc__") or hasattr(module, "__test__"):
+                        try:
+                            suite.addTest(doctest.DocTestSuite(module))
+                        except ValueError: # no tests
+                            pass
+
+class MissingDependencyExcluder:
+    def __init__(self, deps):
+        # deps: { module name : matcher func }
+        self.exclude_matchers = []
+        for mod, matcher in deps.items():
+            try:
+                __import__(mod)
+            except ImportError:
+                self.exclude_matchers.append(matcher)
+        self.tests_missing_deps = []
+    def __call__(self, testname):
+        for matcher in self.exclude_matchers:
+            if matcher(testname):
+                self.tests_missing_deps.append(testname)
+                return True
+        return False
+
+class VersionDependencyExcluder:
+    def __init__(self, deps):
+        # deps: { version : matcher func }
+        from sys import version_info
+        self.exclude_matchers = []
+        for ver, matcher in deps.items():
+            if version_info < ver:
+                self.exclude_matchers.append(matcher)
+        self.tests_missing_deps = []
+    def __call__(self, testname):
+        for matcher in self.exclude_matchers:
+            if matcher(testname):
+                self.tests_missing_deps.append(testname)
+                return True
+        return False
 
 if __name__ == '__main__':
     from optparse import OptionParser
@@ -267,27 +455,67 @@ if __name__ == '__main__':
     parser.add_option("--no-cleanup", dest="cleanup_workdir",
                       action="store_false", default=True,
                       help="do not delete the generated C files (allows passing --no-cython on next run)")
+    parser.add_option("--no-cleanup-sharedlibs", dest="cleanup_sharedlibs",
+                      action="store_false", default=True,
+                      help="do not delete the generated shared libary files (allows manual module experimentation)")
     parser.add_option("--no-cython", dest="with_cython",
                       action="store_false", default=True,
                       help="do not run the Cython compiler, only the C compiler")
+    parser.add_option("--no-c", dest="use_c",
+                      action="store_false", default=True,
+                      help="do not test C compilation")
+    parser.add_option("--no-cpp", dest="use_cpp",
+                      action="store_false", default=True,
+                      help="do not test C++ compilation")
     parser.add_option("--no-unit", dest="unittests",
                       action="store_false", default=True,
                       help="do not run the unit tests")
+    parser.add_option("--no-doctest", dest="doctests",
+                      action="store_false", default=True,
+                      help="do not run the doctests")
     parser.add_option("--no-file", dest="filetests",
                       action="store_false", default=True,
                       help="do not run the file based tests")
+    parser.add_option("--no-pyregr", dest="pyregr",
+                      action="store_false", default=True,
+                      help="do not run the regression tests of CPython in tests/pyregr/")    
+    parser.add_option("--cython-only", dest="cython_only",
+                      action="store_true", default=False,
+                      help="only compile pyx to c, do not run C compiler or run the tests")
+    parser.add_option("--no-refnanny", dest="with_refnanny",
+                      action="store_false", default=True,
+                      help="do not regression test reference counting")
+    parser.add_option("--sys-pyregr", dest="system_pyregr",
+                      action="store_true", default=False,
+                      help="run the regression tests of the CPython installation")
+    parser.add_option("-x", "--exclude", dest="exclude",
+                      action="append", metavar="PATTERN",
+                      help="exclude tests matching the PATTERN")
     parser.add_option("-C", "--coverage", dest="coverage",
                       action="store_true", default=False,
                       help="collect source coverage data for the Compiler")
     parser.add_option("-A", "--annotate", dest="annotate_source",
-                      action="store_true", default=False,
+                      action="store_true", default=True,
                       help="generate annotated HTML versions of the test source files")
+    parser.add_option("--no-annotate", dest="annotate_source",
+                      action="store_false",
+                      help="do not generate annotated HTML versions of the test source files")
     parser.add_option("-v", "--verbose", dest="verbosity",
                       action="count", default=0,
                       help="display test progress, pass twice to print test names")
+    parser.add_option("-T", "--ticket", dest="tickets",
+                      action="append",
+                      help="a bug ticket number to run the respective test in 'tests/bugs'")
 
     options, cmd_args = parser.parse_args()
 
+    if sys.version_info[0] >= 3:
+        # make sure we do not import (or run) Cython itself
+        options.doctests    = False
+        options.with_cython = False
+        options.unittests   = False
+        options.pyregr      = False
+
     if options.coverage:
         import coverage
         coverage.erase()
@@ -306,42 +534,104 @@ if __name__ == '__main__':
     # RUN ALL TESTS!
     ROOTDIR = os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]), 'tests')
     WORKDIR = os.path.join(os.getcwd(), 'BUILD')
+    UNITTEST_MODULE = "Cython"
+    UNITTEST_ROOT = os.path.join(os.getcwd(), UNITTEST_MODULE)
     if WITH_CYTHON:
         if os.path.exists(WORKDIR):
-            shutil.rmtree(WORKDIR, ignore_errors=True)
+            for path in os.listdir(WORKDIR):
+                if path in ("support",): continue
+                shutil.rmtree(os.path.join(WORKDIR, path), ignore_errors=True)
     if not os.path.exists(WORKDIR):
         os.makedirs(WORKDIR)
 
     if WITH_CYTHON:
         from Cython.Compiler.Version import version
-        print("Running tests against Cython %s" % version)
+        sys.stderr.write("Running tests against Cython %s\n" % version)
     else:
-        print("Running tests without Cython.")
-    print("Python %s" % sys.version)
-    print("")
+        sys.stderr.write("Running tests without Cython.\n")
+    sys.stderr.write("Python %s\n" % sys.version)
+    sys.stderr.write("\n")
+
+    if options.with_refnanny:
+        from pyximport.pyxbuild import pyx_to_dll
+        libpath = pyx_to_dll(os.path.join("Cython", "Runtime", "refnanny.pyx"),
+                             build_in_temp=True,
+                             pyxbuild_dir=os.path.join(WORKDIR, "support"))
+        sys.path.insert(0, os.path.split(libpath)[0])
+        CFLAGS.append("-DCYTHON_REFNANNY")
+
+    test_bugs = False
+    if options.tickets:
+        for ticket_number in options.tickets:
+            test_bugs = True
+            cmd_args.append('bugs.*T%s$' % ticket_number)
+    if not test_bugs:
+        for selector in cmd_args:
+            if selector.startswith('bugs'):
+                test_bugs = True
 
     import re
     selectors = [ re.compile(r, re.I|re.U).search for r in cmd_args ]
     if not selectors:
         selectors = [ lambda x:True ]
 
+    # Chech which external modules are not present and exclude tests
+    # which depends on them (by prefix)
+
+    missing_dep_excluder = MissingDependencyExcluder(EXT_DEP_MODULES) 
+    version_dep_excluder = VersionDependencyExcluder(VER_DEP_MODULES) 
+    exclude_selectors = [missing_dep_excluder, version_dep_excluder] # want to pring msg at exit
+
+    if options.exclude:
+        exclude_selectors += [ re.compile(r, re.I|re.U).search for r in options.exclude ]
+
+    languages = []
+    if options.use_c:
+        languages.append('c')
+    if options.use_cpp:
+        languages.append('cpp')
+
     test_suite = unittest.TestSuite()
 
     if options.unittests:
-        collect_unittests(os.getcwd(), test_suite, selectors)
-
-    if options.filetests:
-        filetests = TestBuilder(ROOTDIR, WORKDIR, selectors,
-                                options.annotate_source, options.cleanup_workdir)
-        test_suite.addTests(filetests.build_suite())
+        collect_unittests(UNITTEST_ROOT, UNITTEST_MODULE + ".", test_suite, selectors)
+
+    if options.doctests:
+        collect_doctests(UNITTEST_ROOT, UNITTEST_MODULE + ".", test_suite, selectors)
+
+    if options.filetests and languages:
+        filetests = TestBuilder(ROOTDIR, WORKDIR, selectors, exclude_selectors,
+                                options.annotate_source, options.cleanup_workdir,
+                                options.cleanup_sharedlibs, options.pyregr,
+                                options.cython_only, languages, test_bugs)
+        test_suite.addTest(filetests.build_suite())
+
+    if options.system_pyregr and languages:
+        filetests = TestBuilder(ROOTDIR, WORKDIR, selectors, exclude_selectors,
+                                options.annotate_source, options.cleanup_workdir,
+                                options.cleanup_sharedlibs, True,
+                                options.cython_only, languages)
+        test_suite.addTest(
+            filetests.handle_directory(
+                os.path.join(sys.prefix, 'lib', 'python'+sys.version[:3], 'test'),
+                'pyregr'))
 
     unittest.TextTestRunner(verbosity=options.verbosity).run(test_suite)
 
     if options.coverage:
         coverage.stop()
-        ignored_modules = ('Options', 'Version', 'DebugFlags')
+        ignored_modules = ('Options', 'Version', 'DebugFlags', 'CmdLine')
         modules = [ module for name, module in sys.modules.items()
                     if module is not None and
                     name.startswith('Cython.Compiler.') and 
                     name[len('Cython.Compiler.'):] not in ignored_modules ]
         coverage.report(modules, show_missing=0)
+
+    if missing_dep_excluder.tests_missing_deps:
+        sys.stderr.write("Following tests excluded because of missing dependencies on your system:\n")
+        for test in missing_dep_excluder.tests_missing_deps:
+            sys.stderr.write("   %s\n" % test)
+
+    if options.with_refnanny:
+        import refnanny
+        sys.stderr.write("\n".join([repr(x) for x in refnanny.reflog]))