summaryrefslogtreecommitdiff
path: root/runtests.py
diff options
context:
space:
mode:
Diffstat (limited to 'runtests.py')
-rwxr-xr-xruntests.py763
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):