diff options
Diffstat (limited to 'runtests.py')
-rwxr-xr-x | runtests.py | 763 |
1 files changed, 569 insertions, 194 deletions
diff --git a/runtests.py b/runtests.py index 62005dcb1..dfc1924d3 100755 --- a/runtests.py +++ b/runtests.py @@ -3,6 +3,7 @@ from __future__ import print_function import atexit +import base64 import os import sys import re @@ -27,9 +28,14 @@ try: import platform IS_PYPY = platform.python_implementation() == 'PyPy' IS_CPYTHON = platform.python_implementation() == 'CPython' + IS_GRAAL = platform.python_implementation() == 'GraalVM' except (ImportError, AttributeError): IS_CPYTHON = True IS_PYPY = False + IS_GRAAL = False + +IS_PY2 = sys.version_info[0] < 3 +CAN_SYMLINK = sys.platform != 'win32' and hasattr(os, 'symlink') from io import open as io_open try: @@ -64,11 +70,9 @@ except NameError: basestring = str WITH_CYTHON = True -CY3_DIR = None from distutils.command.build_ext import build_ext as _build_ext from distutils import sysconfig -from distutils import ccompiler _to_clean = [] @atexit.register @@ -96,7 +100,7 @@ def get_distutils_distro(_cache=[]): distutils_distro = Distribution() if sys.platform == 'win32': - # TODO: Figure out why this hackery (see http://thread.gmane.org/gmane.comp.python.cython.devel/8280/). + # TODO: Figure out why this hackery (see https://thread.gmane.org/gmane.comp.python.cython.devel/8280/). config_files = distutils_distro.find_config_files() try: config_files.remove('setup.cfg') @@ -116,7 +120,6 @@ def get_distutils_distro(_cache=[]): EXT_DEP_MODULES = { 'tag:numpy': 'numpy', - 'tag:numpy_old': 'numpy', 'tag:pythran': 'pythran', 'tag:setuptools': 'setuptools.sandbox', 'tag:asyncio': 'asyncio', @@ -236,17 +239,13 @@ def update_linetrace_extension(ext): return ext -def update_old_numpy_extension(ext): - update_numpy_extension(ext, set_api17_macro=False) - - def update_numpy_extension(ext, set_api17_macro=True): import numpy from numpy.distutils.misc_util import get_info ext.include_dirs.append(numpy.get_include()) - if set_api17_macro: + if set_api17_macro and getattr(numpy, '__version__', '') not in ('1.19.0', '1.19.1'): ext.define_macros.append(('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION')) # We need the npymath library for numpy.math. @@ -255,6 +254,22 @@ def update_numpy_extension(ext, set_api17_macro=True): getattr(ext, attr).extend(value) +def update_gdb_extension(ext, _has_gdb=[None]): + # We should probably also check for Python support. + if not include_debugger: + _has_gdb[0] = False + if _has_gdb[0] is None: + try: + subprocess.check_call(["gdb", "--version"]) + except (IOError, subprocess.CalledProcessError): + _has_gdb[0] = False + else: + _has_gdb[0] = True + if not _has_gdb[0]: + return EXCLUDE_EXT + return ext + + def update_openmp_extension(ext): ext.openmp = True language = ext.language @@ -279,27 +294,69 @@ def update_openmp_extension(ext): return EXCLUDE_EXT -def update_cpp11_extension(ext): - """ - update cpp11 extensions that will run on versions of gcc >4.8 - """ - gcc_version = get_gcc_version(ext.language) - if gcc_version: - compiler_version = gcc_version.group(1) - if float(compiler_version) > 4.8: - ext.extra_compile_args.append("-std=c++11") - return ext +def update_cpp_extension(cpp_std, min_gcc_version=None, min_clang_version=None, min_macos_version=None): + def _update_cpp_extension(ext): + """ + Update cpp[cpp_std] extensions that will run on minimum versions of gcc / clang / macos. + """ + # If the extension provides a -std=... option, assume that whatever C compiler we use + # will probably be ok with it. + already_has_std = any( + ca for ca in ext.extra_compile_args + if "-std" in ca and "-stdlib" not in ca + ) + use_gcc = use_clang = already_has_std + + # check for a usable gcc version + gcc_version = get_gcc_version(ext.language) + if gcc_version: + if cpp_std >= 17 and sys.version_info[0] < 3: + # The Python 2.7 headers contain the 'register' modifier + # which gcc warns about in C++17 mode. + ext.extra_compile_args.append('-Wno-register') + if not already_has_std: + compiler_version = gcc_version.group(1) + if not min_gcc_version or float(compiler_version) >= float(min_gcc_version): + use_gcc = True + ext.extra_compile_args.append("-std=c++%s" % cpp_std) + + if use_gcc: + return ext + + # check for a usable clang version + clang_version = get_clang_version(ext.language) + if clang_version: + if cpp_std >= 17 and sys.version_info[0] < 3: + # The Python 2.7 headers contain the 'register' modifier + # which clang warns about in C++17 mode. + ext.extra_compile_args.append('-Wno-register') + if not already_has_std: + compiler_version = clang_version.group(1) + if not min_clang_version or float(compiler_version) >= float(min_clang_version): + use_clang = True + ext.extra_compile_args.append("-std=c++%s" % cpp_std) + if sys.platform == "darwin": + ext.extra_compile_args.append("-stdlib=libc++") + if min_macos_version is not None: + ext.extra_compile_args.append("-mmacosx-version-min=" + min_macos_version) + + if use_clang: + return ext + + # no usable C compiler found => exclude the extension + return EXCLUDE_EXT - clang_version = get_clang_version(ext.language) - if clang_version: - ext.extra_compile_args.append("-std=c++11") - if sys.platform == "darwin": - ext.extra_compile_args.append("-stdlib=libc++") - ext.extra_compile_args.append("-mmacosx-version-min=10.7") - return ext + return _update_cpp_extension - return EXCLUDE_EXT +def require_gcc(version): + def check(ext): + gcc_version = get_gcc_version(ext.language) + if gcc_version: + if float(gcc_version.group(1)) >= float(version): + return ext + return EXCLUDE_EXT + return check def get_cc_version(language): """ @@ -310,7 +367,8 @@ def get_cc_version(language): else: cc = sysconfig.get_config_var('CC') if not cc: - cc = ccompiler.get_default_compiler() + from distutils import ccompiler + cc = ccompiler.get_default_compiler() if not cc: return '' @@ -323,10 +381,9 @@ def get_cc_version(language): env['LC_MESSAGES'] = 'C' try: p = subprocess.Popen([cc, "-v"], stderr=subprocess.PIPE, env=env) - except EnvironmentError: - # Be compatible with Python 3 + except EnvironmentError as exc: warnings.warn("Unable to find the %s compiler: %s: %s" % - (language, os.strerror(sys.exc_info()[1].errno), cc)) + (language, os.strerror(exc.errno), cc)) return '' _, output = p.communicate() return output.decode(locale.getpreferredencoding() or 'ASCII', 'replace') @@ -382,12 +439,16 @@ EXCLUDE_EXT = object() EXT_EXTRAS = { 'tag:numpy' : update_numpy_extension, - 'tag:numpy_old' : update_old_numpy_extension, 'tag:openmp': update_openmp_extension, - 'tag:cpp11': update_cpp11_extension, + 'tag:gdb': update_gdb_extension, + 'tag:cpp11': update_cpp_extension(11, min_gcc_version="4.9", min_macos_version="10.7"), + 'tag:cpp17': update_cpp_extension(17, min_gcc_version="5.0", min_macos_version="10.13"), + 'tag:cpp20': update_cpp_extension(20, min_gcc_version="11.0", min_clang_version="13.0", min_macos_version="10.13"), 'tag:trace' : update_linetrace_extension, 'tag:bytesformat': exclude_extension_in_pyver((3, 3), (3, 4)), # no %-bytes formatting 'tag:no-macos': exclude_extension_on_platform('darwin'), + 'tag:py3only': exclude_extension_in_pyver((2, 7)), + 'tag:cppexecpolicies': require_gcc("9.1") } @@ -395,31 +456,35 @@ EXT_EXTRAS = { VER_DEP_MODULES = { # tests are excluded if 'CurrentPythonVersion OP VersionTuple', i.e. # (2,4) : (operator.lt, ...) excludes ... when PyVer < 2.4.x - (2,7) : (operator.lt, lambda x: x in ['run.withstat_py27', # multi context with statement - 'run.yield_inside_lambda', - 'run.test_dictviews', - 'run.pyclass_special_methods', - 'run.set_literals', - ]), + # 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', 'run.test_raisefrom', + 'run.different_package_names', + 'run.unicode_imports', # encoding problems on appveyor in Py2 'run.reimport_failure', # reimports don't do anything in Py2 + 'run.cpp_stl_cmath_cpp17', + 'run.cpp_stl_cmath_cpp20' ]), (3,): (operator.ge, lambda x: x in ['run.non_future_division', 'compile.extsetslice', 'compile.extdelslice', - 'run.special_methods_T561_py2' + 'run.special_methods_T561_py2', + 'run.builtin_type_inheritance_T608_py2only', ]), (3,3) : (operator.lt, lambda x: x in ['build.package_compilation', + 'build.cythonize_pep420_namespace', 'run.yield_from_py33', 'pyximport.pyximport_namespace', + 'run.qualname', ]), (3,4): (operator.lt, lambda x: x in ['run.py34_signature', 'run.test_unicode', # taken from Py3.7, difficult to backport + 'run.pep442_tp_finalize', + 'run.pep442_tp_finalize_cimport', ]), (3,4,999): (operator.gt, lambda x: x in ['run.initial_file_path', ]), @@ -428,6 +493,15 @@ VER_DEP_MODULES = { 'run.mod__spec__', 'run.pep526_variable_annotations', # typing module 'run.test_exceptions', # copied from Py3.7+ + 'run.time_pxd', # _PyTime_GetSystemClock doesn't exist in 3.4 + 'run.cpython_capi_py35', + 'embedding.embedded', # From the docs, needs Py_DecodeLocale + ]), + (3,7): (operator.lt, lambda x: x in ['run.pycontextvar', + 'run.pep557_dataclasses', # dataclasses module + 'run.test_dataclasses', + ]), + (3,8): (operator.lt, lambda x: x in ['run.special_methods_T561_py38', ]), (3,11,999): (operator.gt, lambda x: x in [ 'run.py_unicode_strings', # Py_UNICODE was removed @@ -480,8 +554,7 @@ def parse_tags(filepath): if tag in ('coding', 'encoding'): continue if tag == 'tags': - tag = 'tag' - print("WARNING: test tags use the 'tag' directive, not 'tags' (%s)" % filepath) + raise RuntimeError("test tags use the 'tag' directive, not 'tags' (%s)" % filepath) if tag not in ('mode', 'tag', 'ticket', 'cython', 'distutils', 'preparse'): print("WARNING: unknown test directive '%s' found (%s)" % (tag, filepath)) values = values.split(',') @@ -491,7 +564,7 @@ def parse_tags(filepath): return tags -list_unchanging_dir = memoize(lambda x: os.listdir(x)) +list_unchanging_dir = memoize(lambda x: os.listdir(x)) # needs lambda to set function attribute @memoize @@ -502,10 +575,23 @@ def _list_pyregr_data_files(test_directory): if is_data_file(filename)] +def import_module_from_file(module_name, file_path, execute=True): + import importlib.util + spec = importlib.util.spec_from_file_location(module_name, file_path) + m = importlib.util.module_from_spec(spec) + if execute: + sys.modules[module_name] = m + spec.loader.exec_module(m) + return m + + def import_ext(module_name, file_path=None): if file_path: - import imp - return imp.load_dynamic(module_name, file_path) + if sys.version_info >= (3, 5): + return import_module_from_file(module_name, file_path) + else: + import imp + return imp.load_dynamic(module_name, file_path) else: try: from importlib import invalidate_caches @@ -537,9 +623,14 @@ class build_ext(_build_ext): class ErrorWriter(object): match_error = re.compile(r'(warning:)?(?:.*:)?\s*([-0-9]+)\s*:\s*([-0-9]+)\s*:\s*(.*)').match - def __init__(self): + def __init__(self, encoding=None): self.output = [] - self.write = self.output.append + self.encoding = encoding + + def write(self, value): + if self.encoding: + value = value.encode('ISO-8859-1').decode(self.encoding) + self.output.append(value) def _collect(self): s = ''.join(self.output) @@ -572,8 +663,8 @@ class Stats(object): self.test_times = defaultdict(float) self.top_tests = defaultdict(list) - def add_time(self, name, language, metric, t): - self.test_counts[metric] += 1 + def add_time(self, name, language, metric, t, count=1): + self.test_counts[metric] += count self.test_times[metric] += t top = self.top_tests[metric] push = heapq.heappushpop if len(top) >= self.top_n else heapq.heappush @@ -615,7 +706,8 @@ class TestBuilder(object): with_pyregr, languages, test_bugs, language_level, common_utility_dir, pythran_dir=None, default_mode='run', stats=None, - add_embedded_test=False): + add_embedded_test=False, add_cython_import=False, + add_cpp_locals_extra_tests=False): self.rootdir = rootdir self.workdir = workdir self.selectors = selectors @@ -627,7 +719,7 @@ class TestBuilder(object): self.cleanup_failures = options.cleanup_failures self.with_pyregr = with_pyregr self.cython_only = options.cython_only - self.doctest_selector = re.compile(options.only_pattern).search if options.only_pattern else None + self.test_selector = re.compile(options.only_pattern).search if options.only_pattern else None self.languages = languages self.test_bugs = test_bugs self.fork = options.fork @@ -638,11 +730,15 @@ class TestBuilder(object): self.default_mode = default_mode self.stats = stats self.add_embedded_test = add_embedded_test + self.add_cython_import = add_cython_import + self.capture = options.capture + self.add_cpp_locals_extra_tests = add_cpp_locals_extra_tests def build_suite(self): suite = unittest.TestSuite() filenames = os.listdir(self.rootdir) filenames.sort() + # TODO: parallelise I/O with a thread pool for the different directories once we drop Py2 support for filename in filenames: path = os.path.join(self.rootdir, filename) if os.path.isdir(path) and filename != TEST_SUPPORT_DIR: @@ -657,7 +753,7 @@ class TestBuilder(object): and (sys.version_info < (3, 8) or sys.platform != 'darwin')): # Non-Windows makefile. if [1 for selector in self.selectors if selector("embedded")] \ - and not [1 for selector in self.exclude_selectors if selector("embedded")]: + and not [1 for selector in self.exclude_selectors if selector("embedded")]: suite.addTest(unittest.makeSuite(EmbedTest)) return suite @@ -696,9 +792,13 @@ class TestBuilder(object): mode = 'pyregr' if ext == '.srctree': + if self.cython_only: + # EndToEnd tests always execute arbitrary build and test code + continue if 'cpp' not in tags['tag'] or 'cpp' in self.languages: - suite.addTest(EndToEndTest( - filepath, workdir, self.cleanup_workdir, stats=self.stats, shard_num=self.shard_num)) + suite.addTest(EndToEndTest(filepath, workdir, + self.cleanup_workdir, stats=self.stats, + capture=self.capture, shard_num=self.shard_num)) continue # Choose the test suite. @@ -717,7 +817,7 @@ class TestBuilder(object): raise KeyError('Invalid test mode: ' + mode) for test in self.build_tests(test_class, path, workdir, - module, mode == 'error', tags): + module, filepath, mode == 'error', tags): suite.addTest(test) if mode == 'run' and ext == '.py' and not self.cython_only and not filename.startswith('test_'): @@ -729,14 +829,16 @@ class TestBuilder(object): ] if not min_py_ver or any(sys.version_info >= min_ver for min_ver in min_py_ver): suite.addTest(PureDoctestTestCase( - module, os.path.join(path, filename), tags, stats=self.stats, shard_num=self.shard_num)) + module, filepath, tags, stats=self.stats, shard_num=self.shard_num)) return suite - def build_tests(self, test_class, path, workdir, module, expect_errors, tags): + def build_tests(self, test_class, path, workdir, module, module_path, expect_errors, tags): warning_errors = 'werror' in tags['tag'] expect_warnings = 'warnings' in tags['tag'] + extra_directives_list = [{}] + if expect_errors: if skip_c(tags) and 'cpp' in self.languages: languages = ['cpp'] @@ -751,9 +853,14 @@ class TestBuilder(object): if 'cpp' in languages and 'no-cpp' in tags['tag']: languages = list(languages) languages.remove('cpp') + if (self.add_cpp_locals_extra_tests and 'cpp' in languages and + 'cpp' in tags['tag'] and not 'no-cpp-locals' in tags['tag']): + extra_directives_list.append({'cpp_locals': True}) if not languages: return [] + language_levels = [2, 3] if 'all_language_levels' in tags['tag'] else [None] + pythran_dir = self.pythran_dir if 'pythran' in tags['tag'] and not pythran_dir and 'cpp' in languages: import pythran.config @@ -763,23 +870,36 @@ class TestBuilder(object): pythran_ext = pythran.config.make_extension() pythran_dir = pythran_ext['include_dirs'][0] + add_cython_import = self.add_cython_import and module_path.endswith('.py') + preparse_list = tags.get('preparse', ['id']) - tests = [ self.build_test(test_class, path, workdir, module, tags, language, + tests = [ self.build_test(test_class, path, workdir, module, module_path, + tags, language, language_level, expect_errors, expect_warnings, warning_errors, preparse, - pythran_dir if language == "cpp" else None) + pythran_dir if language == "cpp" else None, + add_cython_import=add_cython_import, + extra_directives=extra_directives) for language in languages - for preparse in preparse_list ] + for preparse in preparse_list + for language_level in language_levels + for extra_directives in extra_directives_list + ] return tests - def build_test(self, test_class, path, workdir, module, tags, language, - expect_errors, expect_warnings, warning_errors, preparse, pythran_dir): + def build_test(self, test_class, path, workdir, module, module_path, tags, language, language_level, + expect_errors, expect_warnings, warning_errors, preparse, pythran_dir, add_cython_import, + extra_directives): language_workdir = os.path.join(workdir, language) if not os.path.exists(language_workdir): os.makedirs(language_workdir) workdir = os.path.join(language_workdir, module) if preparse != 'id': - workdir += '_%s' % str(preparse) - return test_class(path, workdir, module, tags, + workdir += '_%s' % (preparse,) + if language_level: + workdir += '_cy%d' % (language_level,) + if extra_directives: + workdir += ('_directives_'+ '_'.join('%s_%s' % (k, v) for k,v in extra_directives.items())) + return test_class(path, workdir, module, module_path, tags, language=language, preparse=preparse, expect_errors=expect_errors, @@ -789,15 +909,17 @@ class TestBuilder(object): cleanup_sharedlibs=self.cleanup_sharedlibs, cleanup_failures=self.cleanup_failures, cython_only=self.cython_only, - doctest_selector=self.doctest_selector, + test_selector=self.test_selector, shard_num=self.shard_num, fork=self.fork, - language_level=self.language_level, + language_level=language_level or self.language_level, warning_errors=warning_errors, test_determinism=self.test_determinism, common_utility_dir=self.common_utility_dir, pythran_dir=pythran_dir, - stats=self.stats) + stats=self.stats, + add_cython_import=add_cython_import, + ) def skip_c(tags): @@ -828,17 +950,32 @@ def filter_stderr(stderr_bytes): return stderr_bytes +def filter_test_suite(test_suite, selector): + filtered_tests = [] + for test in test_suite._tests: + if isinstance(test, unittest.TestSuite): + filter_test_suite(test, selector) + elif not selector(test.id()): + continue + filtered_tests.append(test) + test_suite._tests[:] = filtered_tests + + class CythonCompileTestCase(unittest.TestCase): - def __init__(self, test_directory, workdir, module, tags, language='c', preparse='id', + def __init__(self, test_directory, workdir, module, module_path, tags, language='c', preparse='id', expect_errors=False, expect_warnings=False, annotate=False, cleanup_workdir=True, - cleanup_sharedlibs=True, cleanup_failures=True, cython_only=False, doctest_selector=None, + cleanup_sharedlibs=True, cleanup_failures=True, cython_only=False, test_selector=None, fork=True, language_level=2, warning_errors=False, test_determinism=False, shard_num=0, - common_utility_dir=None, pythran_dir=None, stats=None): + common_utility_dir=None, pythran_dir=None, stats=None, add_cython_import=False, + extra_directives=None): + if extra_directives is None: + extra_directives = {} self.test_directory = test_directory self.tags = tags self.workdir = workdir self.module = module + self.module_path = module_path self.language = language self.preparse = preparse self.name = module if self.preparse == "id" else "%s_%s" % (module, preparse) @@ -849,7 +986,7 @@ class CythonCompileTestCase(unittest.TestCase): self.cleanup_sharedlibs = cleanup_sharedlibs self.cleanup_failures = cleanup_failures self.cython_only = cython_only - self.doctest_selector = doctest_selector + self.test_selector = test_selector self.shard_num = shard_num self.fork = fork self.language_level = language_level @@ -858,28 +995,54 @@ class CythonCompileTestCase(unittest.TestCase): self.common_utility_dir = common_utility_dir self.pythran_dir = pythran_dir self.stats = stats + self.add_cython_import = add_cython_import + self.extra_directives = extra_directives unittest.TestCase.__init__(self) def shortDescription(self): - return "[%d] compiling (%s%s) %s" % ( - self.shard_num, self.language, "/pythran" if self.pythran_dir is not None else "", self.name) + return "[%d] compiling (%s%s%s) %s" % ( + self.shard_num, + self.language, + "/cy2" if self.language_level == 2 else "/cy3" if self.language_level == 3 else "", + "/pythran" if self.pythran_dir is not None else "", + self.description_name() + ) + + def description_name(self): + return self.name def setUp(self): from Cython.Compiler import Options self._saved_options = [ (name, getattr(Options, name)) - for name in ('warning_errors', 'clear_to_none', 'error_on_unknown_names', 'error_on_uninitialized') + for name in ( + 'warning_errors', + 'clear_to_none', + 'error_on_unknown_names', + 'error_on_uninitialized', + # 'cache_builtins', # not currently supported due to incorrect global caching + ) ] self._saved_default_directives = list(Options.get_directive_defaults().items()) Options.warning_errors = self.warning_errors if sys.version_info >= (3, 4): Options._directive_defaults['autotestdict'] = False + Options._directive_defaults.update(self.extra_directives) if not os.path.exists(self.workdir): os.makedirs(self.workdir) if self.workdir not in sys.path: sys.path.insert(0, self.workdir) + if self.add_cython_import: + with open(self.module_path, 'rb') as f: + source = f.read() + if b'cython.cimports.' in source: + from Cython.Shadow import CythonCImports + for name in set(re.findall(br"(cython\.cimports(?:\.\w+)+)", source)): + name = name.decode() + sys.modules[name] = CythonCImports(name) + def tearDown(self): from Cython.Compiler import Options for name, value in self._saved_options: @@ -895,6 +1058,13 @@ class CythonCompileTestCase(unittest.TestCase): del sys.modules[self.module] except KeyError: pass + + # remove any stubs of cimported modules in pure Python mode + if self.add_cython_import: + for name in list(sys.modules): + if name.startswith('cython.cimports.'): + del sys.modules[name] + cleanup = self.cleanup_failures or self.success cleanup_c_files = WITH_CYTHON and self.cleanup_workdir and cleanup cleanup_lib_files = self.cleanup_sharedlibs and cleanup @@ -905,13 +1075,17 @@ class CythonCompileTestCase(unittest.TestCase): shutil.rmtree(self.workdir, ignore_errors=True) else: for rmfile in os.listdir(self.workdir): + ext = os.path.splitext(rmfile)[1] if not cleanup_c_files: - if (rmfile[-2:] in (".c", ".h") or - rmfile[-4:] == ".cpp" or - rmfile.endswith(".html") and rmfile.startswith(self.module)): + # Keep C, C++ files, header files, preprocessed sources + # and assembly sources (typically the .i and .s files + # are intentionally generated when -save-temps is given) + if ext in (".c", ".cpp", ".h", ".i", ".ii", ".s"): + continue + if ext == ".html" and rmfile.startswith(self.module): continue - is_shared_obj = rmfile.endswith(".so") or rmfile.endswith(".dll") + is_shared_obj = ext in (".so", ".dll") if not cleanup_lib_files and is_shared_obj: continue @@ -943,8 +1117,9 @@ class CythonCompileTestCase(unittest.TestCase): def runCompileTest(self): return self.compile( - self.test_directory, self.module, self.workdir, - self.test_directory, self.expect_errors, self.expect_warnings, self.annotate) + self.test_directory, self.module, self.module_path, self.workdir, + self.test_directory, self.expect_errors, self.expect_warnings, self.annotate, + self.add_cython_import) def find_module_source_file(self, source_file): if not os.path.exists(source_file): @@ -969,10 +1144,7 @@ class CythonCompileTestCase(unittest.TestCase): fout.write(preparse_func(fin.read())) else: # use symlink on Unix, copy on Windows - try: - copy = os.symlink - except AttributeError: - copy = shutil.copy + copy = os.symlink if CAN_SYMLINK else shutil.copy join = os.path.join for filename in file_list: @@ -985,21 +1157,32 @@ class CythonCompileTestCase(unittest.TestCase): [filename for filename in file_list if not os.path.isfile(os.path.join(workdir, filename))]) - def split_source_and_output(self, test_directory, module, workdir): - source_file = self.find_module_source_file(os.path.join(test_directory, module) + '.pyx') + def split_source_and_output(self, source_file, workdir, add_cython_import=False): + from Cython.Utils import detect_opened_file_encoding + with io_open(source_file, 'rb') as f: + # encoding is passed to ErrorWriter but not used on the source + # since it is sometimes deliberately wrong + encoding = detect_opened_file_encoding(f, default=None) + with io_open(source_file, 'r', encoding='ISO-8859-1') as source_and_output: error_writer = warnings_writer = None - out = io_open(os.path.join(workdir, module + os.path.splitext(source_file)[1]), + out = io_open(os.path.join(workdir, os.path.basename(source_file)), 'w', encoding='ISO-8859-1') try: for line in source_and_output: - if line.startswith("_ERRORS"): + if line.startswith(u"_ERRORS"): out.close() - out = error_writer = ErrorWriter() - elif line.startswith("_WARNINGS"): + out = error_writer = ErrorWriter(encoding=encoding) + elif line.startswith(u"_WARNINGS"): out.close() - out = warnings_writer = ErrorWriter() + out = warnings_writer = ErrorWriter(encoding=encoding) else: + if add_cython_import and line.strip() and not ( + line.startswith(u'#') or line.startswith(u"from __future__ import ")): + # insert "import cython" statement after any directives or future imports + if line != u"import cython\n": + out.write(u"import cython\n") + add_cython_import = False out.write(line) finally: out.close() @@ -1007,18 +1190,16 @@ class CythonCompileTestCase(unittest.TestCase): return (error_writer.geterrors() if error_writer else [], warnings_writer.geterrors() if warnings_writer else []) - def run_cython(self, test_directory, module, targetdir, incdir, annotate, + def run_cython(self, test_directory, module, module_path, targetdir, incdir, annotate, extra_compile_options=None): include_dirs = INCLUDE_DIRS + [os.path.join(test_directory, '..', TEST_SUPPORT_DIR)] if incdir: include_dirs.append(incdir) - if self.preparse == 'id': - source = self.find_module_source_file( - os.path.join(test_directory, module + '.pyx')) - else: - self.copy_files(test_directory, targetdir, [module + '.pyx']) - source = os.path.join(targetdir, module + '.pyx') + if self.preparse != 'id' and test_directory != targetdir: + file_name = os.path.basename(module_path) + self.copy_files(test_directory, targetdir, [file_name]) + module_path = os.path.join(targetdir, file_name) target = os.path.join(targetdir, self.build_target_filename(module)) if extra_compile_options is None: @@ -1031,9 +1212,9 @@ class CythonCompileTestCase(unittest.TestCase): try: CompilationOptions except NameError: - from Cython.Compiler.Main import CompilationOptions + from Cython.Compiler.Options import CompilationOptions from Cython.Compiler.Main import compile as cython_compile - from Cython.Compiler.Main import default_options + from Cython.Compiler.Options import default_options common_utility_include_dir = self.common_utility_dir options = CompilationOptions( @@ -1050,8 +1231,7 @@ class CythonCompileTestCase(unittest.TestCase): common_utility_include_dir = common_utility_include_dir, **extra_compile_options ) - cython_compile(source, options=options, - full_module_name=module) + cython_compile(module_path, options=options, full_module_name=module) def run_distutils(self, test_directory, module, workdir, incdir, extra_extension_args=None): @@ -1089,6 +1269,10 @@ class CythonCompileTestCase(unittest.TestCase): if self.language == 'cpp': # Set the language now as the fixer might need it extension.language = 'c++' + if self.extra_directives.get('cpp_locals'): + extension = update_cpp17_extension(extension) + if extension is EXCLUDE_EXT: + return if 'distutils' in self.tags: from Cython.Build.Dependencies import DistutilsInfo @@ -1120,10 +1304,36 @@ class CythonCompileTestCase(unittest.TestCase): extension = newext or extension if self.language == 'cpp': extension.language = 'c++' + if IS_PY2: + workdir = str(workdir) # work around type check in distutils that disallows unicode strings + build_extension.extensions = [extension] build_extension.build_temp = workdir build_extension.build_lib = workdir - build_extension.run() + + from Cython.Utils import captured_fd, prepare_captured + from distutils.errors import CompileError + + error = None + with captured_fd(2) as get_stderr: + try: + build_extension.run() + except CompileError as exc: + error = str(exc) + stderr = get_stderr() + if stderr and b"Command line warning D9025" in stderr: + # Manually suppress annoying MSVC warnings about overridden CLI arguments. + stderr = b''.join([ + line for line in stderr.splitlines(keepends=True) + if b"Command line warning D9025" not in line + ]) + if stderr: + # The test module name should always be ASCII, but let's not risk encoding failures. + output = b"Compiler output for module " + module.encode('utf-8') + b":\n" + stderr + b"\n" + out = sys.stdout if sys.version_info[0] == 2 else sys.stdout.buffer + out.write(output) + if error is not None: + raise CompileError(u"%s\nCompiler output:\n%s" % (error, prepare_captured(stderr))) finally: os.chdir(cwd) @@ -1145,31 +1355,35 @@ class CythonCompileTestCase(unittest.TestCase): return get_ext_fullpath(module) - def compile(self, test_directory, module, workdir, incdir, - expect_errors, expect_warnings, annotate): + def compile(self, test_directory, module, module_path, workdir, incdir, + expect_errors, expect_warnings, annotate, add_cython_import): expected_errors = expected_warnings = errors = warnings = () - if expect_errors or expect_warnings: + if expect_errors or expect_warnings or add_cython_import: expected_errors, expected_warnings = self.split_source_and_output( - test_directory, module, workdir) + module_path, workdir, add_cython_import) test_directory = workdir + module_path = os.path.join(workdir, os.path.basename(module_path)) if WITH_CYTHON: old_stderr = sys.stderr try: sys.stderr = ErrorWriter() with self.stats.time(self.name, self.language, 'cython'): - self.run_cython(test_directory, module, workdir, incdir, annotate) + self.run_cython(test_directory, module, module_path, workdir, incdir, annotate) errors, warnings = sys.stderr.getall() finally: sys.stderr = old_stderr if self.test_determinism and not expect_errors: workdir2 = workdir + '-again' os.mkdir(workdir2) - self.run_cython(test_directory, module, workdir2, incdir, annotate) + self.run_cython(test_directory, module, module_path, workdir2, incdir, annotate) diffs = [] for file in os.listdir(workdir2): - if (open(os.path.join(workdir, file)).read() - != open(os.path.join(workdir2, file)).read()): + with open(os.path.join(workdir, file)) as fid: + txt1 = fid.read() + with open(os.path.join(workdir2, file)) as fid: + txt2 = fid.read() + if txt1 != txt2: diffs.append(file) os.system('diff -u %s/%s %s/%s > %s/%s.diff' % ( workdir, file, @@ -1216,11 +1430,14 @@ class CythonCompileTestCase(unittest.TestCase): finally: if show_output: stdout = get_stdout and get_stdout().strip() + stderr = get_stderr and filter_stderr(get_stderr()).strip() + if so_path and not stderr: + # normal success case => ignore non-error compiler output + stdout = None if stdout: print_bytes( stdout, header_text="\n=== C/C++ compiler output: =========\n", end=None, file=sys.__stderr__) - stderr = get_stderr and filter_stderr(get_stderr()).strip() if stderr: print_bytes( stderr, header_text="\n=== C/C++ compiler error output: ===\n", @@ -1232,6 +1449,8 @@ class CythonCompileTestCase(unittest.TestCase): def _match_output(self, expected_output, actual_output, write): try: for expected, actual in zip(expected_output, actual_output): + if expected != actual and '\\' in actual and os.sep == '\\' and '/' in expected and '\\' not in expected: + expected = expected.replace('/', '\\') self.assertEqual(expected, actual) if len(actual_output) < len(expected_output): expected = expected_output[len(actual_output)] @@ -1254,12 +1473,8 @@ class CythonRunTestCase(CythonCompileTestCase): from Cython.Compiler import Options Options.clear_to_none = False - def shortDescription(self): - if self.cython_only: - return CythonCompileTestCase.shortDescription(self) - else: - return "[%d] compiling (%s%s) and running %s" % ( - self.shard_num, self.language, "/pythran" if self.pythran_dir is not None else "", self.name) + def description_name(self): + return self.name if self.cython_only else "and running %s" % self.name def run(self, result=None): if result is None: @@ -1270,8 +1485,7 @@ class CythonRunTestCase(CythonCompileTestCase): try: self.success = False ext_so_path = self.runCompileTest() - # Py2.6 lacks "_TextTestResult.skipped" - failures, errors, skipped = len(result.failures), len(result.errors), len(getattr(result, 'skipped', [])) + failures, errors, skipped = len(result.failures), len(result.errors), len(result.skipped) if not self.cython_only and ext_so_path is not None: self.run_tests(result, ext_so_path) if failures == len(result.failures) and errors == len(result.errors): @@ -1301,8 +1515,8 @@ class CythonRunTestCase(CythonCompileTestCase): else: module = module_or_name tests = doctest.DocTestSuite(module) - if self.doctest_selector is not None: - tests._tests[:] = [test for test in tests._tests if self.doctest_selector(test.id())] + if self.test_selector: + filter_test_suite(tests, self.test_selector) with self.stats.time(self.name, self.language, 'run'): tests.run(result) run_forked_test(result, run_test, self.shortDescription(), self.fork) @@ -1404,9 +1618,13 @@ class PureDoctestTestCase(unittest.TestCase): try: self.setUp() - import imp with self.stats.time(self.name, 'py', 'pyimport'): - m = imp.load_source(loaded_module_name, self.module_path) + if sys.version_info >= (3, 5): + m = import_module_from_file(self.module_name, self.module_path) + else: + import imp + m = imp.load_source(loaded_module_name, self.module_path) + try: with self.stats.time(self.name, 'py', 'pyrun'): doctest.DocTestSuite(m).run(result) @@ -1455,10 +1673,6 @@ class PartialTestResult(TextTestResult): TextTestResult.__init__( self, self._StringIO(), True, base_result.dots + base_result.showAll*2) - try: - self.skipped - except AttributeError: - self.skipped = [] # Py2.6 def strip_error_results(self, results): for test_case, error in results: @@ -1483,10 +1697,7 @@ class PartialTestResult(TextTestResult): if output: result.stream.write(output) result.errors.extend(errors) - try: - result.skipped.extend(skipped) - except AttributeError: - pass # Py2.6 + result.skipped.extend(skipped) result.failures.extend(failures) result.testsRun += tests_run @@ -1500,12 +1711,14 @@ class PartialTestResult(TextTestResult): class CythonUnitTestCase(CythonRunTestCase): def shortDescription(self): return "[%d] compiling (%s) tests in %s" % ( - self.shard_num, self.language, self.name) + self.shard_num, self.language, self.description_name()) def run_tests(self, result, ext_so_path): with self.stats.time(self.name, self.language, 'import'): module = import_ext(self.module, ext_so_path) tests = unittest.defaultTestLoader.loadTestsFromModule(module) + if self.test_selector: + filter_test_suite(tests, self.test_selector) with self.stats.time(self.name, self.language, 'run'): tests.run(result) @@ -1592,15 +1805,51 @@ class TestCodeFormat(unittest.TestCase): unittest.TestCase.__init__(self) def runTest(self): + source_dirs = ['Cython', 'Demos', 'docs', 'pyximport', 'tests'] + import pycodestyle - config_file = os.path.join(self.cython_dir, "tox.ini") + config_file = os.path.join(self.cython_dir, "setup.cfg") if not os.path.exists(config_file): - config_file=os.path.join(os.path.dirname(__file__), "tox.ini") - paths = glob.glob(os.path.join(self.cython_dir, "**/*.py"), recursive=True) + config_file = os.path.join(os.path.dirname(__file__), "setup.cfg") + total_errors = 0 + + # checks for .py files + paths = [] + for codedir in source_dirs: + paths += glob.glob(os.path.join(self.cython_dir, codedir + "/**/*.py"), recursive=True) style = pycodestyle.StyleGuide(config_file=config_file) print("") # Fix the first line of the report. result = style.check_files(paths) - self.assertEqual(result.total_errors, 0, "Found code style errors.") + total_errors += result.total_errors + + # checks for non-Python source files + paths = [] + for codedir in ['Cython', 'Demos', 'pyximport']: # source_dirs: + paths += glob.glob(os.path.join(self.cython_dir, codedir + "/**/*.p[yx][xdi]"), recursive=True) + style = pycodestyle.StyleGuide(config_file=config_file, select=[ + # whitespace + "W1", "W2", "W3", + # indentation + "E101", "E111", + ]) + print("") # Fix the first line of the report. + result = style.check_files(paths) + total_errors += result.total_errors + + """ + # checks for non-Python test files + paths = [] + for codedir in ['tests']: + paths += glob.glob(os.path.join(self.cython_dir, codedir + "/**/*.p[yx][xdi]"), recursive=True) + style = pycodestyle.StyleGuide(select=[ + # whitespace + "W1", "W2", "W3", + ]) + result = style.check_files(paths) + total_errors += result.total_errors + """ + + self.assertEqual(total_errors, 0, "Found code style errors.") include_debugger = IS_CPYTHON @@ -1653,13 +1902,13 @@ def collect_doctests(path, module_prefix, suite, selectors, exclude_selectors): return dirname not in ("Mac", "Distutils", "Plex", "Tempita") def file_matches(filename): filename, ext = os.path.splitext(filename) - blacklist = ['libcython', 'libpython', 'test_libcython_in_gdb', - 'TestLibCython'] + excludelist = ['libcython', 'libpython', 'test_libcython_in_gdb', + 'TestLibCython'] return (ext == '.py' and not '~' in filename and not '#' in filename and not filename.startswith('.') and not - filename in blacklist) + filename in excludelist) import doctest for dirpath, dirnames, filenames in os.walk(path): for dir in list(dirnames): @@ -1696,12 +1945,13 @@ class EndToEndTest(unittest.TestCase): """ cython_root = os.path.dirname(os.path.abspath(__file__)) - def __init__(self, treefile, workdir, cleanup_workdir=True, stats=None, shard_num=0): + def __init__(self, treefile, workdir, cleanup_workdir=True, stats=None, capture=True, shard_num=0): self.name = os.path.splitext(os.path.basename(treefile))[0] self.treefile = treefile self.workdir = os.path.join(workdir, self.name) self.cleanup_workdir = cleanup_workdir self.stats = stats + self.capture = capture self.shard_num = shard_num cython_syspath = [self.cython_root] for path in sys.path: @@ -1719,11 +1969,9 @@ class EndToEndTest(unittest.TestCase): def setUp(self): from Cython.TestUtils import unpack_source_tree - _, self.commands = unpack_source_tree(self.treefile, self.workdir) + _, self.commands = unpack_source_tree(self.treefile, self.workdir, self.cython_root) 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: @@ -1737,6 +1985,8 @@ class EndToEndTest(unittest.TestCase): os.chdir(self.old_dir) def _try_decode(self, content): + if not isinstance(content, bytes): + return content try: return content.decode() except UnicodeDecodeError: @@ -1744,38 +1994,44 @@ class EndToEndTest(unittest.TestCase): def runTest(self): self.success = False - 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') env = dict(os.environ) new_path = self.cython_syspath if old_path: - new_path = new_path + os.pathsep + old_path + new_path = new_path + os.pathsep + self.workdir + os.pathsep + old_path env['PYTHONPATH'] = new_path + if not env.get("PYTHONIOENCODING"): + env["PYTHONIOENCODING"] = sys.stdout.encoding or sys.getdefaultencoding() cmd = [] out = [] err = [] - for command_no, command in enumerate(filter(None, commands.splitlines()), 1): + for command_no, command in enumerate(self.commands, 1): with self.stats.time('%s(%d)' % (self.name, command_no), 'c', - 'etoe-build' if ' setup.py ' in command else 'etoe-run'): - p = subprocess.Popen(command, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - shell=True, - env=env) - _out, _err = p.communicate() - cmd.append(command) - out.append(_out) - err.append(_err) - res = p.returncode + 'etoe-build' if 'setup.py' in command else 'etoe-run'): + if self.capture: + p = subprocess.Popen(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE, env=env) + _out, _err = p.communicate() + res = p.returncode + else: + p = subprocess.call(command, env=env) + _out, _err = b'', b'' + res = p + cmd.append(command) + out.append(_out) + err.append(_err) + if res == 0 and b'REFNANNY: ' in _out: res = -1 if res != 0: for c, o, e in zip(cmd, out, err): sys.stderr.write("[%d] %s\n%s\n%s\n\n" % ( self.shard_num, c, self._try_decode(o), self._try_decode(e))) - self.assertEqual(0, res, "non-zero exit status") + sys.stderr.write("Final directory layout of '%s':\n%s\n\n" % ( + self.name, + '\n'.join(os.path.join(dirpath, filename) for dirpath, dirs, files in os.walk(".") for filename in files), + )) + self.assertEqual(0, res, "non-zero exit status, last output was:\n%r\n-- stdout:%s\n-- stderr:%s\n" % ( + ' '.join(command), self._try_decode(out[-1]), self._try_decode(err[-1]))) self.success = True @@ -1810,13 +2066,10 @@ class EmbedTest(unittest.TestCase): 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') - cython = 'cython.py' - if sys.version_info[0] >=3 and CY3_DIR: - cython = os.path.join(CY3_DIR, cython) - cython = os.path.abspath(os.path.join('..', '..', cython)) + cython = os.path.abspath(os.path.join('..', '..', 'cython.py')) try: - subprocess.check_call([ + subprocess.check_output([ "make", "PYTHON='%s'" % sys.executable, "CYTHON='%s'" % cython, @@ -1829,17 +2082,44 @@ class EmbedTest(unittest.TestCase): self.assertTrue(True) # :) +def load_listfile(filename): + # just re-use the FileListExclude implementation + fle = FileListExcluder(filename) + return list(fle.excludes) class MissingDependencyExcluder(object): def __init__(self, deps): # deps: { matcher func : module name } self.exclude_matchers = [] - for matcher, mod in deps.items(): + for matcher, module_name in deps.items(): try: - __import__(mod) + module = __import__(module_name) except ImportError: self.exclude_matchers.append(string_selector(matcher)) + print("Test dependency not found: '%s'" % module_name) + else: + version = self.find_dep_version(module_name, module) + print("Test dependency found: '%s' version %s" % (module_name, version)) self.tests_missing_deps = [] + + def find_dep_version(self, name, module): + try: + version = module.__version__ + except AttributeError: + stdlib_dir = os.path.dirname(shutil.__file__) + os.sep + module_path = getattr(module, '__file__', stdlib_dir) # no __file__? => builtin stdlib module + # GraalPython seems to return None for some unknown reason + if module_path and module_path.startswith(stdlib_dir): + # stdlib module + version = sys.version.partition(' ')[0] + elif '.' in name: + # incrementally look for a parent package with version + name = name.rpartition('.')[0] + return self.find_dep_version(name, __import__(name)) + else: + version = '?.?' + return version + def __call__(self, testname, tags=None): for matcher in self.exclude_matchers: if matcher(testname, tags): @@ -1877,8 +2157,7 @@ class FileListExcluder(object): self.excludes[line.split()[0]] = True def __call__(self, testname, tags=None): - exclude = (testname in self.excludes - or testname.split('.')[-1] in self.excludes) + exclude = any(string_selector(ex)(testname) for ex in self.excludes) if exclude and self.verbose: print("Excluding %s because it's listed in %s" % (testname, self._list_file)) @@ -1920,14 +2199,18 @@ class ShardExcludeSelector(object): # This is an exclude selector so it can override the (include) selectors. # It may not provide uniform distribution (in time or count), but is a # determanistic partition of the tests which is important. + + # Random seed to improve the hash distribution. + _seed = base64.b64decode(b'2ged1EtsGz/GkisJr22UcLeP6n9XIaA5Vby2wM49Wvg=') + def __init__(self, shard_num, shard_count): self.shard_num = shard_num self.shard_count = shard_count - def __call__(self, testname, tags=None, _hash=zlib.crc32, _is_py2=sys.version_info[0] < 3): + def __call__(self, testname, tags=None, _hash=zlib.crc32, _is_py2=IS_PY2): # Cannot use simple hash() here as shard processes might use different hash seeds. # CRC32 is fast and simple, but might return negative values in Py2. - hashval = _hash(testname) & 0x7fffffff if _is_py2 else _hash(testname.encode()) + hashval = _hash(self._seed + testname) & 0x7fffffff if _is_py2 else _hash(self._seed + testname.encode()) return hashval % self.shard_count != self.shard_num @@ -1999,6 +2282,10 @@ def flush_and_terminate(status): def main(): global DISTDIR, WITH_CYTHON + + # Set an environment variable to the top directory + os.environ['CYTHON_PROJECT_DIR'] = os.path.abspath(os.path.dirname(__file__)) + DISTDIR = os.path.join(os.getcwd(), os.path.dirname(sys.argv[0])) from Cython.Compiler import DebugFlags @@ -2010,7 +2297,7 @@ def main(): args.append(arg) from optparse import OptionParser - parser = OptionParser() + parser = OptionParser(usage="usage: %prog [options] [selector ...]") 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)") @@ -2034,6 +2321,9 @@ def main(): parser.add_option("--no-cpp", dest="use_cpp", action="store_false", default=True, help="do not test C++ compilation backend") + parser.add_option("--no-cpp-locals", dest="use_cpp_locals", + action="store_false", default=True, + help="do not rerun select C++ tests with cpp_locals directive") parser.add_option("--no-unit", dest="unittests", action="store_false", default=True, help="do not run the unit tests") @@ -2067,6 +2357,9 @@ def main(): parser.add_option("-x", "--exclude", dest="exclude", action="append", metavar="PATTERN", help="exclude tests matching the PATTERN") + parser.add_option("--listfile", dest="listfile", + action="append", + help="specify a file containing a list of tests to run") parser.add_option("-j", "--shard_count", dest="shard_count", metavar="N", type=int, default=1, help="shard this run into several parallel runs") @@ -2132,6 +2425,10 @@ def main(): help="test whether Cython's output is deterministic") parser.add_option("--pythran-dir", dest="pythran_dir", default=None, help="specify Pythran include directory. This will run the C++ tests using Pythran backend for Numpy") + parser.add_option("--no-capture", dest="capture", default=True, action="store_false", + help="do not capture stdout, stderr in srctree tests. Makes pdb.set_trace interactive") + parser.add_option("--limited-api", dest="limited_api", default=False, action="store_true", + help="Compiles Cython using CPython's LIMITED_API") options, cmd_args = parser.parse_args(args) @@ -2158,7 +2455,18 @@ def main(): if options.xml_output_dir: shutil.rmtree(options.xml_output_dir, ignore_errors=True) + if options.listfile: + for listfile in options.listfile: + cmd_args.extend(load_listfile(listfile)) + + if options.capture and not options.for_debugging: + keep_alive_interval = 10 + else: + keep_alive_interval = None if options.shard_count > 1 and options.shard_num == -1: + if "PYTHONIOENCODING" not in os.environ: + # Make sure subprocesses can print() Unicode text. + os.environ["PYTHONIOENCODING"] = sys.stdout.encoding or sys.getdefaultencoding() import multiprocessing pool = multiprocessing.Pool(options.shard_count) tasks = [(options, cmd_args, shard_num) for shard_num in range(options.shard_count)] @@ -2167,16 +2475,23 @@ def main(): # NOTE: create process pool before time stamper thread to avoid forking issues. total_time = time.time() stats = Stats() - with time_stamper_thread(): - for shard_num, shard_stats, return_code, failure_output in pool.imap_unordered(runtests_callback, tasks): + merged_pipeline_stats = defaultdict(lambda: (0, 0)) + with time_stamper_thread(interval=keep_alive_interval): + for shard_num, shard_stats, pipeline_stats, return_code, failure_output in pool.imap_unordered(runtests_callback, tasks): if return_code != 0: error_shards.append(shard_num) failure_outputs.append(failure_output) sys.stderr.write("FAILED (%s/%s)\n" % (shard_num, options.shard_count)) sys.stderr.write("ALL DONE (%s/%s)\n" % (shard_num, options.shard_count)) + stats.update(shard_stats) + for stage_name, (stage_time, stage_count) in pipeline_stats.items(): + old_time, old_count = merged_pipeline_stats[stage_name] + merged_pipeline_stats[stage_name] = (old_time + stage_time, old_count + stage_count) + pool.close() pool.join() + total_time = time.time() - total_time sys.stderr.write("Sharded tests run in %d seconds (%.1f minutes)\n" % (round(total_time), total_time / 60.)) if error_shards: @@ -2187,15 +2502,30 @@ def main(): else: return_code = 0 else: - with time_stamper_thread(): - _, stats, return_code, _ = runtests(options, cmd_args, coverage) + with time_stamper_thread(interval=keep_alive_interval): + _, stats, merged_pipeline_stats, return_code, _ = runtests(options, cmd_args, coverage) if coverage: if options.shard_count > 1 and options.shard_num == -1: coverage.combine() coverage.stop() + def as_msecs(t, unit=1000000): + # pipeline times are in msecs + return t // unit + float(t % unit) / unit + + pipeline_stats = [ + (as_msecs(stage_time), as_msecs(stage_time) / stage_count, stage_count, stage_name) + for stage_name, (stage_time, stage_count) in merged_pipeline_stats.items() + ] + pipeline_stats.sort(reverse=True) + sys.stderr.write("Most expensive pipeline stages: %s\n" % ", ".join( + "%r: %.2f / %d (%.3f / run)" % (stage_name, total_stage_time, stage_count, stage_time) + for total_stage_time, stage_time, stage_count, stage_name in pipeline_stats[:10] + )) + stats.print_stats(sys.stderr) + if coverage: save_coverage(coverage, options) @@ -2217,20 +2547,30 @@ def time_stamper_thread(interval=10): Print regular time stamps into the build logs to find slow tests. @param interval: time interval in seconds """ + if not interval or interval < 0: + # Do nothing + yield + return + try: _xrange = xrange except NameError: _xrange = range import threading - from datetime import datetime + import datetime from time import sleep interval = _xrange(interval * 4) - now = datetime.now - write = sys.__stderr__.write + now = datetime.datetime.now stop = False + # We capture stderr in some places. + # => make sure we write to the real (original) stderr of the test runner. + stderr = os.dup(2) + def write(s): + os.write(stderr, s if type(s) is bytes else s.encode('ascii')) + def time_stamper(): while True: for _ in interval: @@ -2240,27 +2580,33 @@ def time_stamper_thread(interval=10): write('\n#### %s\n' % now()) thread = threading.Thread(target=time_stamper, name='time_stamper') - thread.setDaemon(True) # Py2.6 ... + thread.daemon = True thread.start() try: yield finally: stop = True thread.join() + os.close(stderr) def configure_cython(options): global CompilationOptions, pyrex_default_options, cython_compile - from Cython.Compiler.Main import \ + from Cython.Compiler.Options import \ CompilationOptions, \ default_options as pyrex_default_options from Cython.Compiler.Options import _directive_defaults as directive_defaults + from Cython.Compiler import Errors Errors.LEVEL = 0 # show all warnings + from Cython.Compiler import Options Options.generate_cleanup_code = 3 # complete cleanup code + from Cython.Compiler import DebugFlags DebugFlags.debug_temp_code_comments = 1 + DebugFlags.debug_no_exception_intercept = 1 # provide better crash output in CI runs + pyrex_default_options['formal_grammar'] = options.use_formal_grammar if options.profile: directive_defaults['profile'] = True @@ -2285,6 +2631,23 @@ def runtests_callback(args): def runtests(options, cmd_args, coverage=None): + # faulthandler should be able to provide a limited traceback + # in the event of a segmentation fault. Only available on Python 3.3+ + try: + import faulthandler + except ImportError: + pass # OK - not essential + else: + faulthandler.enable() + + if sys.platform == "win32" and sys.version_info < (3, 6): + # enable Unicode console output, if possible + try: + import win_unicode_console + except ImportError: + pass + else: + win_unicode_console.enable() WITH_CYTHON = options.with_cython ROOTDIR = os.path.abspath(options.root_dir) @@ -2323,7 +2686,7 @@ def runtests(options, cmd_args, coverage=None): options.cleanup_sharedlibs = False options.fork = False if WITH_CYTHON and include_debugger: - from Cython.Compiler.Main import default_options as compiler_default_options + from Cython.Compiler.Options import default_options as compiler_default_options compiler_default_options['gdb_debug'] = True compiler_default_options['output_dir'] = os.getcwd() @@ -2340,6 +2703,10 @@ def runtests(options, cmd_args, coverage=None): sys.path.insert(0, os.path.split(libpath)[0]) CDEFS.append(('CYTHON_REFNANNY', '1')) + if options.limited_api: + CFLAGS.append("-DCYTHON_LIMITED_API=1") + CFLAGS.append('-Wno-unused-function') + if xml_output_dir and options.fork: # doesn't currently work together sys.stderr.write("Disabling forked testing to support XML test output\n") @@ -2388,7 +2755,7 @@ def runtests(options, cmd_args, coverage=None): if options.exclude: exclude_selectors += [ string_selector(r) for r in options.exclude ] - if not COMPILER_HAS_INT128 or not IS_CPYTHON: + if not COMPILER_HAS_INT128: exclude_selectors += [RegExSelector('int128')] if options.shard_num > -1: @@ -2398,8 +2765,14 @@ def runtests(options, cmd_args, coverage=None): bug_files = [ ('bugs.txt', True), ('pypy_bugs.txt', IS_PYPY), + ('pypy2_bugs.txt', IS_PYPY and IS_PY2), + ('pypy_crash_bugs.txt', IS_PYPY), + ('pypy_implementation_detail_bugs.txt', IS_PYPY), + ('graal_bugs.txt', IS_GRAAL), + ('limited_api_bugs.txt', options.limited_api), ('windows_bugs.txt', sys.platform == 'win32'), - ('cygwin_bugs.txt', sys.platform == 'cygwin') + ('cygwin_bugs.txt', sys.platform == 'cygwin'), + ('windows_bugs_39.txt', sys.platform == 'win32' and sys.version_info[:2] == (3, 9)) ] exclude_selectors += [ @@ -2428,8 +2801,8 @@ def runtests(options, cmd_args, coverage=None): sys.stderr.write("Backends: %s\n" % ','.join(backends)) languages = backends - if 'TRAVIS' in os.environ and sys.platform == 'darwin' and 'cpp' in languages: - bugs_file_name = 'travis_macos_cpp_bugs.txt' + if 'CI' in os.environ and sys.platform == 'darwin' and 'cpp' in languages: + bugs_file_name = 'macos_cpp_bugs.txt' exclude_selectors += [ FileListExcluder(os.path.join(ROOTDIR, bugs_file_name), verbose=verbose_excludes) @@ -2457,8 +2830,10 @@ def runtests(options, cmd_args, coverage=None): filetests = TestBuilder(ROOTDIR, WORKDIR, selectors, exclude_selectors, options, options.pyregr, languages, test_bugs, options.language_level, common_utility_dir, - options.pythran_dir, add_embedded_test=True, stats=stats) + options.pythran_dir, add_embedded_test=True, stats=stats, + add_cpp_locals_extra_tests=options.use_cpp_locals) test_suite.addTest(filetests.build_suite()) + if options.examples and languages: examples_workdir = os.path.join(WORKDIR, 'examples') for subdirectory in glob.glob(os.path.join(options.examples_dir, "*/")): @@ -2466,7 +2841,7 @@ def runtests(options, cmd_args, coverage=None): options, options.pyregr, languages, test_bugs, options.language_level, common_utility_dir, options.pythran_dir, - default_mode='compile', stats=stats) + default_mode='compile', stats=stats, add_cython_import=True) test_suite.addTest(filetests.build_suite()) if options.system_pyregr and languages: @@ -2503,10 +2878,7 @@ def runtests(options, cmd_args, coverage=None): else: text_runner_options = {} if options.failfast: - if sys.version_info < (2, 7): - sys.stderr.write("--failfast not supported with Python < 2.7\n") - else: - text_runner_options['failfast'] = True + text_runner_options['failfast'] = True test_runner = unittest.TextTestRunner(verbosity=options.verbosity, **text_runner_options) if options.pyximport_py: @@ -2548,6 +2920,9 @@ def runtests(options, cmd_args, coverage=None): if common_utility_dir and options.shard_num < 0 and options.cleanup_workdir: shutil.rmtree(common_utility_dir) + from Cython.Compiler.Pipeline import get_timings + pipeline_stats = get_timings() + 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: @@ -2564,7 +2939,7 @@ def runtests(options, cmd_args, coverage=None): else: failure_output = "".join(collect_failure_output(result)) - return options.shard_num, stats, result_code, failure_output + return options.shard_num, stats, pipeline_stats, result_code, failure_output def collect_failure_output(result): |