X-Git-Url: http://git.tremily.us/?a=blobdiff_plain;f=runtests.py;h=8d9664524e6f9222a8889db908e8b78fa7eb4d7b;hb=3e75aa6c6086c43b5d0226aa49b6c134e047462b;hp=ff21f206f16a7a58bd8848f7baf4dd09e97dfdd7;hpb=6899588a03831cd19ba76d2553232a524c7196f8;p=cython.git diff --git a/runtests.py b/runtests.py index ff21f206..8d966452 100644 --- a/runtests.py +++ b/runtests.py @@ -10,6 +10,7 @@ import unittest import doctest import operator import tempfile +import traceback try: from StringIO import StringIO except ImportError: @@ -20,6 +21,11 @@ try: except ImportError: import pickle +try: + import threading +except ImportError: # No threads, no problems + threading = None + WITH_CYTHON = True @@ -28,14 +34,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', 'wrappers', 'pyregr'] +TEST_DIRS = ['compile', 'errors', 'run', 'wrappers', 'pyregr', 'build'] 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, - 'pstats' : re.compile('.*\.pstats_.*').match + 'pstats' : re.compile('.*\.pstats_.*').match, + 'posix' : re.compile('.*\.posix_.*').match, } def get_numpy_include_dirs(): @@ -49,14 +56,25 @@ EXT_DEP_INCLUDES = [ VER_DEP_MODULES = { # tests are excluded if 'CurrentPythonVersion OP VersionTuple', i.e. - # (2,4) : (operator.le, ...) excludes ... when PyVer <= 2.4.x - (2,4) : (operator.le, lambda x: x in ['run.extern_builtins_T258' + # (2,4) : (operator.lt, ...) excludes ... when PyVer < 2.4.x + (2,4) : (operator.lt, lambda x: x in ['run.extern_builtins_T258', + 'run.builtin_sorted' + ]), + (2,5) : (operator.lt, lambda x: x in ['run.any', + 'run.all', ]), - (2,6) : (operator.lt, lambda x: x in ['run.print_function' + (2,6) : (operator.lt, lambda x: x in ['run.print_function', + 'run.cython3', ]), + # The next line should start (3,); but this is a dictionary, so + # we can only have one (3,) key. Since 2.7 is supposed to be the + # last 2.x release, things would have to change drastically for this + # to be unsafe... + (2,999): (operator.lt, lambda x: x in ['run.special_methods_T561_py3']), (3,): (operator.ge, lambda x: x in ['run.non_future_division', 'compile.extsetslice', - 'compile.extdelslice']), + 'compile.extdelslice', + 'run.special_methods_T561_py2']), } INCLUDE_DIRS = [ d for d in os.getenv('INCLUDE', '').split(os.pathsep) if d ] @@ -106,7 +124,7 @@ class ErrorWriter(object): class TestBuilder(object): def __init__(self, rootdir, workdir, selectors, exclude_selectors, annotate, cleanup_workdir, cleanup_sharedlibs, with_pyregr, cython_only, - languages, test_bugs, fork): + languages, test_bugs, fork, language_level): self.rootdir = rootdir self.workdir = workdir self.selectors = selectors @@ -119,6 +137,7 @@ class TestBuilder(object): self.languages = languages self.test_bugs = test_bugs self.fork = fork + self.language_level = language_level def build_suite(self): suite = unittest.TestSuite() @@ -151,6 +170,11 @@ class TestBuilder(object): filenames = os.listdir(path) filenames.sort() for filename in filenames: + if context == "build" and filename.endswith(".srctree"): + if not [ 1 for match in self.selectors if match(filename) ]: + continue + suite.addTest(EndToEndTest(filename, workdir, self.cleanup_workdir)) + continue if not (filename.endswith(".pyx") or filename.endswith(".py")): continue if filename.startswith('.'): continue # certain emacs backup files @@ -174,6 +198,9 @@ class TestBuilder(object): for test in self.build_tests(test_class, path, workdir, module, expect_errors): suite.addTest(test) + if context == 'run' and filename.endswith('.py'): + # additionally test file in real Python + suite.addTest(PureDoctestTestCase(module, os.path.join(path, filename))) return suite def build_tests(self, test_class, path, workdir, module, expect_errors): @@ -204,12 +231,14 @@ class TestBuilder(object): cleanup_workdir=self.cleanup_workdir, cleanup_sharedlibs=self.cleanup_sharedlibs, cython_only=self.cython_only, - fork=self.fork) + fork=self.fork, + language_level=self.language_level) class CythonCompileTestCase(unittest.TestCase): 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): + cleanup_sharedlibs=True, cython_only=False, fork=True, + language_level=2): self.test_directory = test_directory self.workdir = workdir self.module = module @@ -220,6 +249,7 @@ class CythonCompileTestCase(unittest.TestCase): self.cleanup_sharedlibs = cleanup_sharedlibs self.cython_only = cython_only self.fork = fork + self.language_level = language_level unittest.TestCase.__init__(self) def shortDescription(self): @@ -290,21 +320,24 @@ class CythonCompileTestCase(unittest.TestCase): 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'), - 'w', 'ISO-8859-1') - for line in source_and_output: - last_line = line - if line.startswith("_ERRORS"): - out.close() - out = ErrorWriter() - else: - out.write(line) + source_file = self.find_module_source_file(os.path.join(test_directory, module) + '.pyx') + source_and_output = codecs.open(source_file, 'rU', 'ISO-8859-1') + try: + out = codecs.open(os.path.join(workdir, module + os.path.splitext(source_file)[1]), + 'w', 'ISO-8859-1') + for line in source_and_output: + last_line = line + if line.startswith("_ERRORS"): + out.close() + out = ErrorWriter() + else: + out.write(line) + finally: + source_and_output.close() try: geterrors = out.geterrors except AttributeError: + out.close() return [] else: return geterrors() @@ -323,6 +356,7 @@ class CythonCompileTestCase(unittest.TestCase): annotate = annotate, use_listing_file = False, cplus = self.language == 'cpp', + language_level = self.language_level, generate_pxi = False, evaluate_tree_assertions = True, ) @@ -406,9 +440,12 @@ class CythonRunTestCase(CythonCompileTestCase): result.startTest(self) try: self.setUp() - self.runCompileTest() - if not self.cython_only: - self.run_doctests(self.module, result) + try: + self.runCompileTest() + if not self.cython_only: + self.run_doctests(self.module, result) + finally: + check_thread_termination() except Exception: result.addError(self, sys.exc_info()) result.stopTest(self) @@ -449,7 +486,6 @@ class CythonRunTestCase(CythonCompileTestCase): output = open(result_file, 'wb') pickle.dump(partial_result.data(), output) except: - import traceback traceback.print_exc() finally: try: output.close() @@ -478,6 +514,39 @@ class CythonRunTestCase(CythonCompileTestCase): try: os.unlink(result_file) except: pass +class PureDoctestTestCase(unittest.TestCase): + def __init__(self, module_name, module_path): + self.module_name = module_name + self.module_path = module_path + unittest.TestCase.__init__(self, 'run') + + def shortDescription(self): + return "running pure doctests in %s" % self.module_name + + def run(self, result=None): + if result is None: + result = self.defaultTestResult() + loaded_module_name = 'pure_doctest__' + self.module_name + result.startTest(self) + try: + self.setUp() + + import imp + m = imp.load_source(loaded_module_name, self.module_path) + try: + doctest.DocTestSuite(m).run(result) + finally: + del m + if loaded_module_name in sys.modules: + del sys.modules[loaded_module_name] + check_thread_termination() + except Exception: + result.addError(self, sys.exc_info()) + result.stopTest(self) + try: + self.tearDown() + except Exception: + pass is_private_field = re.compile('^_[^_]').match @@ -542,8 +611,11 @@ class CythonUnitTestCase(CythonCompileTestCase): result.startTest(self) try: self.setUp() - self.runCompileTest() - unittest.defaultTestLoader.loadTestsFromName(self.module).run(result) + try: + self.runCompileTest() + unittest.defaultTestLoader.loadTestsFromName(self.module).run(result) + finally: + check_thread_termination() except Exception: result.addError(self, sys.exc_info()) result.stopTest(self) @@ -599,7 +671,10 @@ def collect_doctests(path, module_prefix, suite, selectors): for f in filenames: if file_matches(f): if not f.endswith('.py'): continue - filepath = os.path.join(dirpath, f)[:-len(".py")] + filepath = os.path.join(dirpath, f) + if os.path.getsize(filepath) == 0: continue + if 'no doctest' in open(filepath).next(): continue + filepath = filepath[:-len(".py")] modulename = module_prefix + filepath[len(path)+1:].replace(os.path.sep, '.') if not [ 1 for match in selectors if match(modulename) ]: continue @@ -612,6 +687,60 @@ def collect_doctests(path, module_prefix, suite, selectors): except ValueError: # no tests pass + +class EndToEndTest(unittest.TestCase): + """ + This is a test of build/*.srctree files, where srctree defines a full + directory structure and its header gives a list of commands to run. + """ + cython_root = os.path.dirname(os.path.abspath(__file__)) + + def __init__(self, treefile, workdir, cleanup_workdir=True): + self.treefile = treefile + self.workdir = os.path.join(workdir, os.path.splitext(treefile)[0]) + self.cleanup_workdir = cleanup_workdir + cython_syspath = self.cython_root + for path in sys.path[::-1]: + if path.startswith(self.cython_root): + # Py3 installation and refnanny build prepend their + # fixed paths to sys.path => prefer that over the + # generic one + cython_syspath = path + os.pathsep + cython_syspath + self.cython_syspath = cython_syspath + unittest.TestCase.__init__(self) + + def shortDescription(self): + return "End-to-end %s" % self.treefile + + def setUp(self): + from Cython.TestUtils import unpack_source_tree + _, self.commands = unpack_source_tree(os.path.join('tests', 'build', self.treefile), self.workdir) + self.old_dir = os.getcwd() + os.chdir(self.workdir) + if self.workdir not in sys.path: + sys.path.insert(0, self.workdir) + + def tearDown(self): + if self.cleanup_workdir: + shutil.rmtree(self.workdir) + os.chdir(self.old_dir) + + def runTest(self): + commands = (self.commands + .replace("CYTHON", "PYTHON %s" % os.path.join(self.cython_root, 'cython.py')) + .replace("PYTHON", sys.executable)) + old_path = os.environ.get('PYTHONPATH') + try: + os.environ['PYTHONPATH'] = self.cython_syspath + os.pathsep + (old_path or '') + print(os.environ['PYTHONPATH']) + self.assertEqual(0, os.system(commands)) + finally: + if old_path: + os.environ['PYTHONPATH'] = old_path + else: + del os.environ['PYTHONPATH'] + + # TODO: Support cython_freeze needed here as well. # TODO: Windows support. @@ -634,8 +763,18 @@ class EmbedTest(unittest.TestCase): os.chdir(self.old_dir) def test_embed(self): + from distutils import sysconfig + libname = sysconfig.get_config_var('LIBRARY') + libdir = sysconfig.get_config_var('LIBDIR') + if not os.path.isdir(libdir) or libname not in os.listdir(libdir): + libdir = os.path.join(os.path.dirname(sys.executable), '..', 'lib') + if not os.path.isdir(libdir) or libname not in os.listdir(libdir): + libdir = os.path.join(libdir, 'python%d.%d' % sys.version_info[:2], 'config') + if not os.path.isdir(libdir) or libname not in os.listdir(libdir): + # report the error for the original directory + libdir = sysconfig.get_config_var('LIBDIR') self.assert_(os.system( - "make PYTHON='%s' test > make.output" % sys.executable) == 0) + "make PYTHON='%s' LIBDIR1='%s' test > make.output" % (sys.executable, libdir)) == 0) try: os.remove('make.output') except OSError: @@ -678,10 +817,14 @@ class FileListExcluder: def __init__(self, list_file): self.excludes = {} - for line in open(list_file).readlines(): - line = line.strip() - if line and line[0] != '#': - self.excludes[line.split()[0]] = True + f = open(list_file) + try: + for line in f.readlines(): + line = line.strip() + if line and line[0] != '#': + self.excludes[line.split()[0]] = True + finally: + f.close() def __call__(self, testname): return testname in self.excludes or testname.split('.')[-1] in self.excludes @@ -696,7 +839,7 @@ def refactor_for_py3(distdir, cy3_dir): if not os.path.exists(cy3_dir): os.makedirs(cy3_dir) import distutils.log as dlog - dlog.set_threshold(dlog.DEBUG) + dlog.set_threshold(dlog.INFO) copydir_run_2to3(distdir, cy3_dir, fixer_names=fixers, template = ''' global-exclude * @@ -706,8 +849,38 @@ def refactor_for_py3(distdir, cy3_dir): ''') sys.path.insert(0, cy3_dir) +class PendingThreadsError(RuntimeError): + pass -if __name__ == '__main__': +threads_seen = [] + +def check_thread_termination(ignore_seen=True): + if threading is None: # no threading enabled in CPython + return + current = threading.currentThread() + blocking_threads = [] + for t in threading.enumerate(): + if not t.isAlive() or t == current: + continue + t.join(timeout=2) + if t.isAlive(): + if not ignore_seen: + blocking_threads.append(t) + continue + for seen in threads_seen: + if t is seen: + break + else: + threads_seen.append(t) + blocking_threads.append(t) + if not blocking_threads: + return + sys.stderr.write("warning: left-over threads found after running test:\n") + for t in blocking_threads: + sys.stderr.write('...%s\n' % repr(t)) + raise PendingThreadsError("left-over threads found after running test") + +def main(): from optparse import OptionParser parser = OptionParser() parser.add_option("--no-cleanup", dest="cleanup_workdir", @@ -770,6 +943,9 @@ if __name__ == '__main__': parser.add_option("-T", "--ticket", dest="tickets", action="append", help="a bug ticket number to run the respective test in 'tests/*'") + parser.add_option("-3", dest="language_level", + action="store_const", const=3, default=2, + help="set language level to Python 3 (useful for running the CPython regression 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, @@ -784,12 +960,13 @@ if __name__ == '__main__': if sys.version_info[0] >= 3: options.doctests = False - options.pyregr = False if options.with_cython: try: # try if Cython is installed in a Py3 version import Cython.Compiler.Main except Exception: + # back out anything the import process loaded, then + # 2to3 the Cython sources to make them re-importable cy_modules = [ name for name in sys.modules if name == 'Cython' or name.startswith('Cython.') ] for name in cy_modules: @@ -807,14 +984,15 @@ if __name__ == '__main__': if options.coverage or options.coverage_xml: if not WITH_CYTHON: - options.coverage = False + options.coverage = options.coverage_xml = False else: from coverage import coverage as _coverage - coverage = _coverage() + coverage = _coverage(branch=True) coverage.erase() coverage.start() if WITH_CYTHON: + global CompilationOptions, pyrex_default_options, cython_compile from Cython.Compiler.Main import \ CompilationOptions, \ default_options as pyrex_default_options, \ @@ -837,13 +1015,13 @@ if __name__ == '__main__': if not os.path.exists(WORKDIR): os.makedirs(WORKDIR) + sys.stderr.write("Python %s\n" % sys.version) + sys.stderr.write("\n") if WITH_CYTHON: from Cython.Compiler.Version import version sys.stderr.write("Running tests against Cython %s\n" % version) else: 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 @@ -858,6 +1036,11 @@ if __name__ == '__main__': sys.stderr.write("Disabling forked testing to support XML test output\n") options.fork = False + if WITH_CYTHON and options.language_level == 3: + sys.stderr.write("Using Cython language level 3.\n") + + sys.stderr.write("\n") + test_bugs = False if options.tickets: for ticket_number in options.tickets: @@ -908,7 +1091,7 @@ if __name__ == '__main__': options.annotate_source, options.cleanup_workdir, options.cleanup_sharedlibs, options.pyregr, options.cython_only, languages, test_bugs, - options.fork) + options.fork, options.language_level) test_suite.addTest(filetests.build_suite()) if options.system_pyregr and languages: @@ -916,7 +1099,7 @@ if __name__ == '__main__': options.annotate_source, options.cleanup_workdir, options.cleanup_sharedlibs, True, options.cython_only, languages, test_bugs, - options.fork) + options.fork, options.language_level) test_suite.addTest( filetests.handle_directory( os.path.join(sys.prefix, 'lib', 'python'+sys.version[:3], 'test'), @@ -952,7 +1135,29 @@ if __name__ == '__main__': import refnanny sys.stderr.write("\n".join([repr(x) for x in refnanny.reflog])) + print("ALL DONE") + if options.exit_ok: - sys.exit(0) + return_code = 0 else: - sys.exit(not result.wasSuccessful()) + return_code = not result.wasSuccessful() + + try: + check_thread_termination(ignore_seen=False) + sys.exit(return_code) + except PendingThreadsError: + # normal program exit won't kill the threads, do it the hard way here + os._exit(return_code) + +if __name__ == '__main__': + try: + main() + except SystemExit: # <= Py2.4 ... + raise + except Exception: + traceback.print_exc() + try: + check_thread_termination(ignore_seen=False) + except PendingThreadsError: + # normal program exit won't kill the threads, do it the hard way here + os._exit(1)