better debug output
[cython.git] / runtests.py
index 2ff4a3acbea8494cdf79c776f9bd42f23725d6c0..d4921d5bacddfa5a239260f642a10983df31717f 100644 (file)
@@ -28,13 +28,15 @@ from distutils.core import Extension
 from distutils.command.build_ext import build_ext as _build_ext
 distutils_distro = Distribution()
 
-TEST_DIRS = ['compile', 'errors', 'run', 'pyregr']
-TEST_RUN_DIRS = ['run', 'pyregr']
+TEST_DIRS = ['compile', 'errors', 'run', 'wrappers', 'pyregr']
+TEST_RUN_DIRS = ['run', 'wrappers', 'pyregr']
 
 # 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
+    'numpy' : re.compile('.*\.numpy_.*').match,
+    'pstats' : re.compile('.*\.pstats_.*').match,
+    'posix' : re.compile('.*\.posix_.*').match,
 }
 
 def get_numpy_include_dirs():
@@ -47,8 +49,16 @@ EXT_DEP_INCLUDES = [
 ]
 
 VER_DEP_MODULES = {
-# such as:
-#    (2,4) : (operator.le, lambda x: x in ['run.set']),
+    # tests are excluded if 'CurrentPythonVersion OP VersionTuple', i.e.
+    # (2,4) : (operator.le, ...) excludes ... when PyVer <= 2.4.x
+    (2,5) : (operator.lt, lambda x: x in ['run.any',
+                                          'run.all',
+                                          ]),
+    (2,4) : (operator.le, lambda x: x in ['run.extern_builtins_T258'
+                                          ]),
+    (2,6) : (operator.lt, lambda x: x in ['run.print_function',
+                                          'run.cython3',
+                                          ]),
     (3,): (operator.ge, lambda x: x in ['run.non_future_division',
                                         'compile.extsetslice',
                                         'compile.extdelslice']),
@@ -173,7 +183,10 @@ class TestBuilder(object):
 
     def build_tests(self, test_class, path, workdir, module, expect_errors):
         if expect_errors:
-            languages = self.languages[:1]
+            if 'cpp' in module and 'cpp' in self.languages:
+                languages = ['cpp']
+            else:
+                languages = self.languages[:1]
         else:
             languages = self.languages
         if 'cpp' in module and 'c' in languages:
@@ -199,10 +212,10 @@ class TestBuilder(object):
                           fork=self.fork)
 
 class CythonCompileTestCase(unittest.TestCase):
-    def __init__(self, directory, workdir, module, language='c',
+    def __init__(self, test_directory, workdir, module, language='c',
                  expect_errors=False, annotate=False, cleanup_workdir=True,
                  cleanup_sharedlibs=True, cython_only=False, fork=True):
-        self.directory = directory
+        self.test_directory = test_directory
         self.workdir = workdir
         self.module = module
         self.language = language
@@ -256,8 +269,8 @@ class CythonCompileTestCase(unittest.TestCase):
         self.runCompileTest()
 
     def runCompileTest(self):
-        self.compile(self.directory, self.module, self.workdir,
-                     self.directory, self.expect_errors, self.annotate)
+        self.compile(self.test_directory, self.module, self.workdir,
+                     self.test_directory, self.expect_errors, self.annotate)
 
     def find_module_source_file(self, source_file):
         if not os.path.exists(source_file):
@@ -268,8 +281,21 @@ class CythonCompileTestCase(unittest.TestCase):
         target = '%s.%s' % (module_name, self.language)
         return target
 
-    def split_source_and_output(self, directory, module, workdir):
-        source_file = os.path.join(directory, module) + '.pyx'
+    def copy_related_files(self, test_directory, target_directory, module_name):
+        is_related = re.compile('%s_.*[.].*' % module_name).match
+        for filename in os.listdir(test_directory):
+            if is_related(filename):
+                shutil.copy(os.path.join(test_directory, filename),
+                            target_directory)
+
+    def find_source_files(self, workdir, module_name):
+        is_related = re.compile('%s_.*[.]%s' % (module_name, self.language)).match
+        return [self.build_target_filename(module_name)] + [
+            filename for filename in os.listdir(workdir)
+            if is_related(filename) and os.path.isfile(os.path.join(workdir, filename)) ]
+
+    def split_source_and_output(self, test_directory, module, workdir):
+        source_file = os.path.join(test_directory, module) + '.pyx'
         source_and_output = codecs.open(
             self.find_module_source_file(source_file), 'rU', 'ISO-8859-1')
         out = codecs.open(os.path.join(workdir, module + '.pyx'),
@@ -288,12 +314,12 @@ class CythonCompileTestCase(unittest.TestCase):
         else:
             return geterrors()
 
-    def run_cython(self, directory, module, targetdir, incdir, annotate):
+    def run_cython(self, test_directory, module, targetdir, incdir, annotate):
         include_dirs = INCLUDE_DIRS[:]
         if incdir:
             include_dirs.append(incdir)
         source = self.find_module_source_file(
-            os.path.join(directory, module + '.pyx'))
+            os.path.join(test_directory, module + '.pyx'))
         target = os.path.join(targetdir, self.build_target_filename(module))
         options = CompilationOptions(
             pyrex_default_options,
@@ -303,12 +329,13 @@ class CythonCompileTestCase(unittest.TestCase):
             use_listing_file = False,
             cplus = self.language == 'cpp',
             generate_pxi = False,
-            evaluate_tree_assertions = True,
+#            evaluate_tree_assertions = True,
+            evaluate_tree_assertions = False,
             )
         cython_compile(source, options=options,
                        full_module_name=module)
 
-    def run_distutils(self, module, workdir, incdir):
+    def run_distutils(self, test_directory, module, workdir, incdir):
         cwd = os.getcwd()
         os.chdir(workdir)
         try:
@@ -321,9 +348,10 @@ class CythonCompileTestCase(unittest.TestCase):
             for match, get_additional_include_dirs in EXT_DEP_INCLUDES:
                 if match(module):
                     ext_include_dirs += get_additional_include_dirs()
+            self.copy_related_files(test_directory, workdir, module)
             extension = Extension(
                 module,
-                sources = [self.build_target_filename(module)],
+                sources = self.find_source_files(workdir, module),
                 include_dirs = ext_include_dirs,
                 extra_compile_args = CFLAGS,
                 )
@@ -336,19 +364,19 @@ class CythonCompileTestCase(unittest.TestCase):
         finally:
             os.chdir(cwd)
 
-    def compile(self, directory, module, workdir, incdir,
+    def compile(self, test_directory, module, workdir, incdir,
                 expect_errors, annotate):
         expected_errors = errors = ()
         if expect_errors:
             expected_errors = self.split_source_and_output(
-                directory, module, workdir)
-            directory = workdir
+                test_directory, module, workdir)
+            test_directory = workdir
 
         if WITH_CYTHON:
             old_stderr = sys.stderr
             try:
                 sys.stderr = ErrorWriter()
-                self.run_cython(directory, module, workdir, incdir, annotate)
+                self.run_cython(test_directory, module, workdir, incdir, annotate)
                 errors = sys.stderr.geterrors()
             finally:
                 sys.stderr = old_stderr
@@ -372,7 +400,7 @@ class CythonCompileTestCase(unittest.TestCase):
                 raise
         else:
             if not self.cython_only:
-                self.run_distutils(module, workdir, incdir)
+                self.run_distutils(test_directory, module, workdir, incdir)
 
 class CythonRunTestCase(CythonCompileTestCase):
     def shortDescription(self):
@@ -403,30 +431,32 @@ class CythonRunTestCase(CythonCompileTestCase):
 
         # fork to make sure we do not keep the tested module loaded
         result_handle, result_file = tempfile.mkstemp()
+        os.close(result_handle)
         child_id = os.fork()
         if not child_id:
             result_code = 0
             try:
-                output = os.fdopen(result_handle, 'wb')
-                tests = None
                 try:
-                    partial_result = PartialTestResult(result)
-                    tests = doctest.DocTestSuite(module_name)
-                    tests.run(partial_result)
-                    gc.collect()
-                except Exception:
-                    if tests is None:
-                        # importing failed, try to fake a test class
-                        tests = _FakeClass(
-                            failureException=None,
-                            shortDescription = self.shortDescription,
-                            **{module_name: None})
-                    partial_result.addError(tests, sys.exc_info())
-                    result_code = 1
-                pickle.dump(partial_result.data(), output)
-            except:
-                import traceback
-                traceback.print_exc()
+                    tests = None
+                    try:
+                        partial_result = PartialTestResult(result)
+                        tests = doctest.DocTestSuite(module_name)
+                        tests.run(partial_result)
+                        gc.collect()
+                    except Exception:
+                        if tests is None:
+                            # importing failed, try to fake a test class
+                            tests = _FakeClass(
+                                failureException=sys.exc_info()[1],
+                                _shortDescription=self.shortDescription(),
+                                module_name=None)
+                        partial_result.addError(tests, sys.exc_info())
+                        result_code = 1
+                    output = open(result_file, 'wb')
+                    pickle.dump(partial_result.data(), output)
+                except:
+                    import traceback
+                    traceback.print_exc()
             finally:
                 try: output.close()
                 except: pass
@@ -434,6 +464,13 @@ class CythonRunTestCase(CythonCompileTestCase):
 
         try:
             cid, result_code = os.waitpid(child_id, 0)
+            # os.waitpid returns the child's result code in the
+            # upper byte of result_code, and the signal it was
+            # killed by in the lower byte
+            if result_code & 255:
+                raise Exception("Tests in module '%s' were unexpectedly killed by signal %d"%
+                                (module_name, result_code & 255))
+            result_code = result_code >> 8
             if result_code in (0,1):
                 input = open(result_file, 'rb')
                 try:
@@ -442,7 +479,7 @@ class CythonRunTestCase(CythonCompileTestCase):
                     input.close()
             if result_code:
                 raise Exception("Tests in module '%s' exited with status %d" %
-                                (module_name, result_code >> 8))
+                                (module_name, result_code))
         finally:
             try: os.unlink(result_file)
             except: pass
@@ -474,7 +511,7 @@ class PartialTestResult(_TextTestResult):
                 if attr_name == '_dt_test':
                     test_case._dt_test = _FakeClass(
                         name=test_case._dt_test.name)
-                else:
+                elif attr_name != '_shortDescription':
                     setattr(test_case, attr_name, None)
 
     def data(self):
@@ -487,7 +524,7 @@ class PartialTestResult(_TextTestResult):
         """Static method for merging the result back into the main
         result object.
         """
-        errors, failures, tests_run, output = data
+        failures, errors, tests_run, output = data
         if output:
             result.stream.write(output)
         result.errors.extend(errors)
@@ -591,17 +628,24 @@ class EmbedTest(unittest.TestCase):
     def setUp(self):
         self.old_dir = os.getcwd()
         os.chdir(self.working_dir)
-        os.system("make clean > /dev/null")
+        os.system(
+            "make PYTHON='%s' clean > /dev/null" % sys.executable)
     
     def tearDown(self):
         try:
-            os.system("make clean > /dev/null")
+            os.system(
+                "make PYTHON='%s' clean > /dev/null" % sys.executable)
         except:
             pass
         os.chdir(self.old_dir)
         
     def test_embed(self):
-        self.assert_(os.system("make test > make.output") == 0)
+        self.assert_(os.system(
+            "make PYTHON='%s' test > make.output" % sys.executable) == 0)
+        try:
+            os.remove('make.output')
+        except OSError:
+            pass
 
 class MissingDependencyExcluder:
     def __init__(self, deps):
@@ -646,7 +690,28 @@ class FileListExcluder:
                 self.excludes[line.split()[0]] = True
                 
     def __call__(self, testname):
-        return testname.split('.')[-1] in self.excludes
+        return testname in self.excludes or testname.split('.')[-1] in self.excludes
+
+def refactor_for_py3(distdir, cy3_dir):
+    # need to convert Cython sources first
+    import lib2to3.refactor
+    from distutils.util import copydir_run_2to3
+    fixers = [ fix for fix in lib2to3.refactor.get_fixers_from_package("lib2to3.fixes")
+               if fix.split('fix_')[-1] not in ('next',)
+               ]
+    if not os.path.exists(cy3_dir):
+        os.makedirs(cy3_dir)
+    import distutils.log as dlog
+    dlog.set_threshold(dlog.DEBUG)
+    copydir_run_2to3(distdir, cy3_dir, fixer_names=fixers,
+                     template = '''
+                     global-exclude *
+                     graft Cython
+                     recursive-exclude Cython *
+                     recursive-include Cython *.py *.pyx *.pxd
+                     ''')
+    sys.path.insert(0, cy3_dir)
+
 
 if __name__ == '__main__':
     from optparse import OptionParser
@@ -696,6 +761,9 @@ if __name__ == '__main__':
     parser.add_option("-C", "--coverage", dest="coverage",
                       action="store_true", default=False,
                       help="collect source coverage data for the Compiler")
+    parser.add_option("--coverage-xml", dest="coverage_xml",
+                      action="store_true", default=False,
+                      help="collect source coverage data for the Compiler in XML format")
     parser.add_option("-A", "--annotate", dest="annotate_source",
                       action="store_true", default=True,
                       help="generate annotated HTML versions of the test source files")
@@ -707,7 +775,12 @@ if __name__ == '__main__':
                       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'")
+                      help="a bug ticket number to run the respective test in 'tests/*'")
+    parser.add_option("--xml-output", dest="xml_output_dir", metavar="DIR",
+                      help="write test results in XML to directory DIR")
+    parser.add_option("--exit-ok", dest="exit_ok", default=False,
+                      action="store_true",
+                      help="exit without error code even on test failures")
 
     options, cmd_args = parser.parse_args()
 
@@ -715,44 +788,37 @@ if __name__ == '__main__':
     ROOTDIR = os.path.join(DISTDIR, 'tests')
     WORKDIR = os.path.join(os.getcwd(), 'BUILD')
 
-    if sys.version_info >= (3,1):
-        options.doctests    = False
-        options.unittests   = False
-        options.pyregr      = False
+    if sys.version_info[0] >= 3:
+        options.doctests = False
         if options.with_cython:
-            # need to convert Cython sources first
-            import lib2to3.refactor
-            from distutils.util import copydir_run_2to3
-            fixers = [ fix for fix in lib2to3.refactor.get_fixers_from_package("lib2to3.fixes")
-                       if fix.split('fix_')[-1] not in ('next',)
-                       ]
-            cy3_dir = os.path.join(WORKDIR, 'Cy3')
-            if not os.path.exists(cy3_dir):
-                os.makedirs(cy3_dir)
-            import distutils.log as dlog
-            dlog.set_threshold(dlog.DEBUG)
-            copydir_run_2to3(DISTDIR, cy3_dir, fixer_names=fixers,
-                             template = '''
-                             global-exclude *
-                             graft Cython
-                             recursive-exclude Cython *
-                             recursive-include Cython *.py *.pyx *.pxd
-                             ''')
-            sys.path.insert(0, cy3_dir)
-    elif sys.version_info[0] >= 3:
-        # make sure we do not import (or run) Cython itself
-        options.with_cython = False
-        options.doctests    = False
-        options.unittests   = False
-        options.pyregr      = False
-
-    if options.coverage:
-        import coverage
-        coverage.erase()
-        coverage.start()
+            try:
+                # try if Cython is installed in a Py3 version
+                import Cython.Compiler.Main
+            except Exception:
+                cy_modules = [ name for name in sys.modules
+                               if name == 'Cython' or name.startswith('Cython.') ]
+                for name in cy_modules:
+                    del sys.modules[name]
+                # hasn't been refactored yet - do it now
+                cy3_dir = os.path.join(WORKDIR, 'Cy3')
+                if sys.version_info >= (3,1):
+                    refactor_for_py3(DISTDIR, cy3_dir)
+                elif os.path.isdir(cy3_dir):
+                    sys.path.insert(0, cy3_dir)
+                else:
+                    options.with_cython = False
 
     WITH_CYTHON = options.with_cython
 
+    if options.coverage or options.coverage_xml:
+        if not WITH_CYTHON:
+            options.coverage = options.coverage_xml = False
+        else:
+            from coverage import coverage as _coverage
+            coverage = _coverage(branch=True)
+            coverage.erase()
+            coverage.start()
+
     if WITH_CYTHON:
         from Cython.Compiler.Main import \
             CompilationOptions, \
@@ -792,6 +858,11 @@ if __name__ == '__main__':
         sys.path.insert(0, os.path.split(libpath)[0])
         CFLAGS.append("-DCYTHON_REFNANNY=1")
 
+    if options.xml_output_dir and options.fork:
+        # doesn't currently work together
+        sys.stderr.write("Disabling forked testing to support XML test output\n")
+        options.fork = False
+
     test_bugs = False
     if options.tickets:
         for ticket_number in options.tickets:
@@ -856,16 +927,26 @@ if __name__ == '__main__':
                 os.path.join(sys.prefix, 'lib', 'python'+sys.version[:3], 'test'),
                 'pyregr'))
 
-    result = unittest.TextTestRunner(verbosity=options.verbosity).run(test_suite)
+    if options.xml_output_dir:
+        from Cython.Tests.xmlrunner import XMLTestRunner
+        test_runner = XMLTestRunner(output=options.xml_output_dir,
+                                    verbose=options.verbosity > 0)
+    else:
+        test_runner = unittest.TextTestRunner(verbosity=options.verbosity)
+
+    result = test_runner.run(test_suite)
 
-    if options.coverage:
+    if options.coverage or options.coverage_xml:
         coverage.stop()
         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 options.coverage:
+            coverage.report(modules, show_missing=0)
+        if options.coverage_xml:
+            coverage.xml_report(modules, outfile="coverage-report.xml")
 
     if missing_dep_excluder.tests_missing_deps:
         sys.stderr.write("Following tests excluded because of missing dependencies on your system:\n")
@@ -876,4 +957,7 @@ if __name__ == '__main__':
         import refnanny
         sys.stderr.write("\n".join([repr(x) for x in refnanny.reflog]))
 
-    sys.exit(not result.wasSuccessful()
+    if options.exit_ok:
+        sys.exit(0)
+    else:
+        sys.exit(not result.wasSuccessful())