runtests.py 110 KB
Newer Older
1
#!/usr/bin/env python
2

3 4
from __future__ import print_function

5
import atexit
6
import base64
7 8 9
import os
import sys
import re
10
import gc
11
import heapq
12
import locale
13
import shutil
14
import time
15 16
import unittest
import doctest
17
import operator
Robert Bradshaw's avatar
Robert Bradshaw committed
18
import subprocess
19
import tempfile
20
import traceback
Robert Bradshaw's avatar
Robert Bradshaw committed
21
import warnings
22
import zlib
23
import glob
24
from contextlib import contextmanager
25
from collections import defaultdict
Robert Bradshaw's avatar
Robert Bradshaw committed
26

27 28 29 30 31 32 33
try:
    import platform
    IS_PYPY = platform.python_implementation() == 'PyPy'
    IS_CPYTHON = platform.python_implementation() == 'CPython'
except (ImportError, AttributeError):
    IS_CPYTHON = True
    IS_PYPY = False
34

35
IS_PY2 = sys.version_info[0] < 3
36
CAN_SYMLINK = sys.platform != 'win32' and hasattr(os, 'symlink')
37

38 39 40 41 42
from io import open as io_open
try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO  # doesn't accept 'str' in Py2
43 44 45 46 47 48

try:
    import cPickle as pickle
except ImportError:
    import pickle

49 50 51 52 53
try:
    import threading
except ImportError: # No threads, no problems
    threading = None

54 55 56
try:
    from unittest import SkipTest
except ImportError:
57 58
    class SkipTest(Exception):  # don't raise, only provided to allow except-ing it!
        pass
59
    def skip_test(reason):
60
        sys.stderr.write("Skipping test: %s\n" % reason)
61 62 63 64
else:
    def skip_test(reason):
        raise SkipTest(reason)

Stefan Behnel's avatar
Stefan Behnel committed
65 66 67 68 69
try:
    basestring
except NameError:
    basestring = str

70 71 72
WITH_CYTHON = True

from distutils.command.build_ext import build_ext as _build_ext
Mark Florisson's avatar
Mark Florisson committed
73
from distutils import sysconfig
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
_to_clean = []

@atexit.register
def _cleanup_files():
    """
    This is only used on Cygwin to clean up shared libraries that are unsafe
    to delete while the test suite is running.
    """

    for filename in _to_clean:
        if os.path.isdir(filename):
            shutil.rmtree(filename, ignore_errors=True)
        else:
            try:
                os.remove(filename)
            except OSError:
                pass

92

93 94 95
def get_distutils_distro(_cache=[]):
    if _cache:
        return _cache[0]
Unknown's avatar
Unknown committed
96
    # late import to accommodate for setuptools override
97 98 99 100
    from distutils.dist import Distribution
    distutils_distro = Distribution()

    if sys.platform == 'win32':
101
        # TODO: Figure out why this hackery (see https://thread.gmane.org/gmane.comp.python.cython.devel/8280/).
102
        config_files = distutils_distro.find_config_files()
103 104 105 106
        try:
            config_files.remove('setup.cfg')
        except ValueError:
            pass
107 108 109
        distutils_distro.parse_config_files(config_files)

        cfgfiles = distutils_distro.find_config_files()
110 111 112 113
        try:
            cfgfiles.remove('setup.cfg')
        except ValueError:
            pass
114 115 116
        distutils_distro.parse_config_files(cfgfiles)
    _cache.append(distutils_distro)
    return distutils_distro
Robert Bradshaw's avatar
Robert Bradshaw committed
117 118


119
EXT_DEP_MODULES = {
120
    'tag:numpy':     'numpy',
121
    'tag:pythran':  'pythran',
122
    'tag:setuptools':  'setuptools.sandbox',
123 124 125 126
    'tag:asyncio':  'asyncio',
    'tag:pstats':   'pstats',
    'tag:posix':    'posix',
    'tag:array':    'array',
127
    'tag:coverage': 'Cython.Coverage',
128
    'Coverage':     'Cython.Coverage',
129
    'tag:ipython':  'IPython.testing.globalipapp',
130
    'tag:jedi':     'jedi_BROKEN_AND_DISABLED',
131
    'tag:test.support': 'test.support',  # support module for CPython unit tests
132 133
}

134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
def patch_inspect_isfunction():
    import inspect
    orig_isfunction = inspect.isfunction
    def isfunction(obj):
        return orig_isfunction(obj) or type(obj).__name__ == 'cython_function_or_method'
    isfunction._orig_isfunction = orig_isfunction
    inspect.isfunction = isfunction

def unpatch_inspect_isfunction():
    import inspect
    try:
        orig_isfunction = inspect.isfunction._orig_isfunction
    except AttributeError:
        pass
    else:
        inspect.isfunction = orig_isfunction

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
def def_to_cdef(source):
    '''
    Converts the module-level def methods into cdef methods, i.e.

        @decorator
        def foo([args]):
            """
            [tests]
            """
            [body]

    becomes

        def foo([args]):
            """
            [tests]
            """
            return foo_c([args])

        cdef foo_c([args]):
            [body]
    '''
    output = []
    skip = False
    def_node = re.compile(r'def (\w+)\(([^()*]*)\):').match
    lines = iter(source.split('\n'))
    for line in lines:
        if not line.strip():
            output.append(line)
            continue

        if skip:
            if line[0] != ' ':
                skip = False
            else:
                continue

        if line[0] == '@':
            skip = True
            continue

        m = def_node(line)
        if m:
            name = m.group(1)
            args = m.group(2)
            if args:
                args_no_types = ", ".join(arg.split()[-1] for arg in args.split(','))
            else:
                args_no_types = ""
            output.append("def %s(%s):" % (name, args_no_types))
Robert Bradshaw's avatar
Robert Bradshaw committed
201
            line = next(lines)
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
            if '"""' in line:
                has_docstring = True
                output.append(line)
                for line in lines:
                    output.append(line)
                    if '"""' in line:
                        break
            else:
                has_docstring = False
            output.append("    return %s_c(%s)" % (name, args_no_types))
            output.append('')
            output.append("cdef %s_c(%s):" % (name, args))
            if not has_docstring:
                output.append(line)

        else:
            output.append(line)

    return '\n'.join(output)

222 223 224 225 226 227 228

def exclude_extension_in_pyver(*versions):
    def check(ext):
        return EXCLUDE_EXT if sys.version_info[:2] in versions else ext
    return check


229 230 231 232 233 234
def exclude_extension_on_platform(*platforms):
    def check(ext):
        return EXCLUDE_EXT if sys.platform in platforms else ext
    return check


235 236 237 238
def update_linetrace_extension(ext):
    ext.define_macros.append(('CYTHON_TRACE', 1))
    return ext

239

240
def update_numpy_extension(ext, set_api17_macro=True):
241
    import numpy
242 243
    from numpy.distutils.misc_util import get_info

244
    ext.include_dirs.append(numpy.get_include())
245

246
    if set_api17_macro and getattr(numpy, '__version__', '') not in ('1.19.0', '1.19.1'):
247 248
        ext.define_macros.append(('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION'))

249 250 251 252 253
    # We need the npymath library for numpy.math.
    # This is typically a static-only library.
    for attr, value in get_info('npymath').items():
        getattr(ext, attr).extend(value)

254

255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
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


Mark Florisson's avatar
Mark Florisson committed
271
def update_openmp_extension(ext):
272
    ext.openmp = True
Mark Florisson's avatar
Mark Florisson committed
273 274
    language = ext.language

275 276 277 278
    if sys.platform == 'win32' and sys.version_info[:2] == (3,4):
        # OpenMP tests fail in appveyor in Py3.4 -> just ignore them, EoL of Py3.4 is early 2019...
        return EXCLUDE_EXT

Mark Florisson's avatar
Mark Florisson committed
279 280 281 282 283 284 285 286 287 288
    if language == 'cpp':
        flags = OPENMP_CPP_COMPILER_FLAGS
    else:
        flags = OPENMP_C_COMPILER_FLAGS

    if flags:
        compile_flags, link_flags = flags
        ext.extra_compile_args.extend(compile_flags.split())
        ext.extra_link_args.extend(link_flags.split())
        return ext
289 290
    elif sys.platform == 'win32':
        return ext
Mark Florisson's avatar
Mark Florisson committed
291 292 293

    return EXCLUDE_EXT

294

295
def update_cpp11_extension(ext):
Mark Florisson's avatar
Mark Florisson committed
296
    """
297 298 299
        update cpp11 extensions that will run on versions of gcc >4.8
    """
    gcc_version = get_gcc_version(ext.language)
300
    already_has_std = any(ca for ca in ext.extra_compile_args if "-std" in ca)
301
    if gcc_version:
302
        compiler_version = gcc_version.group(1)
303
        if float(compiler_version) > 4.8 and not already_has_std:
304
            ext.extra_compile_args.append("-std=c++11")
305
        return ext
306

Stefan Behnel's avatar
Stefan Behnel committed
307 308
    clang_version = get_clang_version(ext.language)
    if clang_version:
309 310
        if not already_has_std:
            ext.extra_compile_args.append("-std=c++11")
311
        if sys.platform == "darwin":
312 313
            ext.extra_compile_args.append("-stdlib=libc++")
            ext.extra_compile_args.append("-mmacosx-version-min=10.7")
314 315
        return ext

316
    return EXCLUDE_EXT
Mark Florisson's avatar
Mark Florisson committed
317

318 319 320 321 322 323 324
def update_cpp17_extension(ext):
    """
        update cpp17 extensions that will run on versions of gcc >=5.0
    """
    gcc_version = get_gcc_version(ext.language)
    if gcc_version:
        compiler_version = gcc_version.group(1)
325 326 327 328
        if 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')
329 330 331 332 333 334 335
        if float(compiler_version) >= 5.0:
            ext.extra_compile_args.append("-std=c++17")
        return ext

    clang_version = get_clang_version(ext.language)
    if clang_version:
        ext.extra_compile_args.append("-std=c++17")
336 337 338 339
        if 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')
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
        if sys.platform == "darwin":
          ext.extra_compile_args.append("-stdlib=libc++")
          ext.extra_compile_args.append("-mmacosx-version-min=10.13")
        return ext

    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
355

356
def get_cc_version(language):
357 358
    """
        finds gcc version using Popen
Mark Florisson's avatar
Mark Florisson committed
359 360 361 362 363
    """
    if language == 'cpp':
        cc = sysconfig.get_config_var('CXX')
    else:
        cc = sysconfig.get_config_var('CC')
364
    if not cc:
365 366
        from distutils import ccompiler
        cc = ccompiler.get_default_compiler()
367 368

    if not cc:
369
        return ''
Mark Florisson's avatar
Mark Florisson committed
370

371 372 373
    # For some reason, cc can be e.g. 'gcc -pthread'
    cc = cc.split()[0]

374 375 376
    # Force english output
    env = os.environ.copy()
    env['LC_MESSAGES'] = 'C'
Mark Florisson's avatar
Mark Florisson committed
377
    try:
378
        p = subprocess.Popen([cc, "-v"], stderr=subprocess.PIPE, env=env)
Robert Bradshaw's avatar
Robert Bradshaw committed
379 380 381 382
    except EnvironmentError:
        # Be compatible with Python 3
        warnings.warn("Unable to find the %s compiler: %s: %s" %
                      (language, os.strerror(sys.exc_info()[1].errno), cc))
383
        return ''
Robert Bradshaw's avatar
Robert Bradshaw committed
384
    _, output = p.communicate()
385 386 387 388 389 390 391 392 393
    return output.decode(locale.getpreferredencoding() or 'ASCII', 'replace')


def get_gcc_version(language):
    matcher = re.compile(r"gcc version (\d+\.\d+)").search
    return matcher(get_cc_version(language))


def get_clang_version(language):
394
    matcher = re.compile(r"clang(?:-|\s+version\s+)(\d+\.\d+)").search
395
    return matcher(get_cc_version(language))
396 397 398 399 400 401 402 403 404 405 406


def get_openmp_compiler_flags(language):
    """
    As of gcc 4.2, it supports OpenMP 2.5. Gcc 4.4 implements 3.0. We don't
    (currently) check for other compilers.

    returns a two-tuple of (CFLAGS, LDFLAGS) to build the OpenMP extension
    """
    gcc_version = get_gcc_version(language)

407
    if not gcc_version:
408 409 410 411
        if sys.platform == 'win32':
            return '/openmp', ''
        else:
            return None # not gcc - FIXME: do something about other compilers
412

413 414 415 416
    # gcc defines "__int128_t", assume that at least all 64 bit architectures have it
    global COMPILER_HAS_INT128
    COMPILER_HAS_INT128 = getattr(sys, 'maxsize', getattr(sys, 'maxint', 0)) > 2**60

417
    compiler_version = gcc_version.group(1)
Mark Florisson's avatar
Mark Florisson committed
418 419 420
    if compiler_version and compiler_version.split('.') >= ['4', '2']:
        return '-fopenmp', '-fopenmp'

421 422 423 424
try:
    locale.setlocale(locale.LC_ALL, '')
except locale.Error:
    pass
425

426 427
COMPILER = None
COMPILER_HAS_INT128 = False
Mark Florisson's avatar
Mark Florisson committed
428 429 430 431 432 433
OPENMP_C_COMPILER_FLAGS = get_openmp_compiler_flags('c')
OPENMP_CPP_COMPILER_FLAGS = get_openmp_compiler_flags('cpp')

# Return this from the EXT_EXTRAS matcher callback to exclude the extension
EXCLUDE_EXT = object()

434 435
EXT_EXTRAS = {
    'tag:numpy' : update_numpy_extension,
Mark Florisson's avatar
Mark Florisson committed
436
    'tag:openmp': update_openmp_extension,
437
    'tag:gdb': update_gdb_extension,
438
    'tag:cpp11': update_cpp11_extension,
439
    'tag:cpp17': update_cpp17_extension,
440
    'tag:trace' : update_linetrace_extension,
441
    'tag:bytesformat':  exclude_extension_in_pyver((3, 3), (3, 4)),  # no %-bytes formatting
442
    'tag:no-macos':  exclude_extension_on_platform('darwin'),
443
    'tag:py3only':  exclude_extension_in_pyver((2, 7)),
444
    'tag:cppexecpolicies': require_gcc("9.1")
445
}
446

447

Robert Bradshaw's avatar
Robert Bradshaw committed
448
# TODO: use tags
449
VER_DEP_MODULES = {
450
    # tests are excluded if 'CurrentPythonVersion OP VersionTuple', i.e.
Stefan Behnel's avatar
Stefan Behnel committed
451
    # (2,4) : (operator.lt, ...) excludes ... when PyVer < 2.4.x
452

453 454 455 456
    # 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...
457 458
    (2,999): (operator.lt, lambda x: x in ['run.special_methods_T561_py3',
                                           'run.test_raisefrom',
459
                                           'run.different_package_names',
460
                                           'run.unicode_imports',  # encoding problems on appveyor in Py2
461
                                           'run.reimport_failure',  # reimports don't do anything in Py2
462
                                           ]),
463
    (3,): (operator.ge, lambda x: x in ['run.non_future_division',
Stefan Behnel's avatar
Stefan Behnel committed
464
                                        'compile.extsetslice',
465
                                        'compile.extdelslice',
466
                                        'run.special_methods_T561_py2',
467
                                        ]),
468
    (3,3) : (operator.lt, lambda x: x in ['build.package_compilation',
469
                                          'build.cythonize_pep420_namespace',
470
                                          'run.yield_from_py33',
471
                                          'pyximport.pyximport_namespace',
472
                                          'run.qualname',
473
                                          ]),
474
    (3,4): (operator.lt, lambda x: x in ['run.py34_signature',
475
                                         'run.test_unicode',  # taken from Py3.7, difficult to backport
476
                                         ]),
477 478
    (3,4,999): (operator.gt, lambda x: x in ['run.initial_file_path',
                                             ]),
479
    (3,5): (operator.lt, lambda x: x in ['run.py35_pep492_interop',
Stefan Behnel's avatar
Stefan Behnel committed
480
                                         'run.py35_asyncio_async_def',
481
                                         'run.mod__spec__',
482
                                         'run.pep526_variable_annotations',  # typing module
483
                                         'run.test_exceptions',  # copied from Py3.7+
484
                                         'run.time_pxd',  # _PyTime_GetSystemClock doesn't exist in 3.4
485
                                         ]),
486
    (3,7): (operator.lt, lambda x: x in ['run.pycontextvar',
487
                                         'run.pep557_dataclasses',  # dataclasses module
488
                                         ]),
489 490
}

491
INCLUDE_DIRS = [ d for d in os.getenv('INCLUDE', '').split(os.pathsep) if d ]
492
CFLAGS = os.getenv('CFLAGS', '').split()
493
CCACHE = os.getenv('CYTHON_RUNTESTS_CCACHE', '').split()
494
CDEFS = []
495
TEST_SUPPORT_DIR = 'testsupport'
496

497
BACKENDS = ['c', 'cpp']
498

499 500 501
UTF8_BOM_BYTES = r'\xef\xbb\xbf'.encode('ISO-8859-1').decode('unicode_escape')


502 503 504 505 506 507 508 509 510 511
def memoize(f):
    uncomputed = object()
    f._cache = {}
    def func(*args):
        res = f._cache.get(args, uncomputed)
        if res is uncomputed:
            res = f._cache[args] = f(*args)
        return res
    return func

512

Robert Bradshaw's avatar
cleanup  
Robert Bradshaw committed
513
@memoize
Robert Bradshaw's avatar
Robert Bradshaw committed
514 515
def parse_tags(filepath):
    tags = defaultdict(list)
516
    parse_tag = re.compile(r'#\s*(\w+)\s*:(.*)$').match
517
    with io_open(filepath, encoding='ISO-8859-1', errors='ignore') as f:
518
        for line in f:
519 520
            # ignore BOM-like bytes and whitespace
            line = line.lstrip(UTF8_BOM_BYTES).strip()
521
            if not line:
522 523 524 525
                if tags:
                    break  # assume all tags are in one block
                else:
                    continue
526 527
            if line[0] != '#':
                break
528 529 530 531 532 533 534 535
            parsed = parse_tag(line)
            if parsed:
                tag, values = parsed.groups()
                if tag in ('coding', 'encoding'):
                    continue
                if tag == 'tags':
                    tag = 'tag'
                    print("WARNING: test tags use the 'tag' directive, not 'tags' (%s)" % filepath)
536
                if tag not in ('mode', 'tag', 'ticket', 'cython', 'distutils', 'preparse'):
537 538 539 540 541
                    print("WARNING: unknown test directive '%s' found (%s)" % (tag, filepath))
                values = values.split(',')
                tags[tag].extend(filter(None, [value.strip() for value in values]))
            elif tags:
                break  # assume all tags are in one block
Robert Bradshaw's avatar
Robert Bradshaw committed
542 543
    return tags

544

Stefan Behnel's avatar
Stefan Behnel committed
545
list_unchanging_dir = memoize(lambda x: os.listdir(x))  # needs lambda to set function attribute
Robert Bradshaw's avatar
Robert Bradshaw committed
546

Stefan Behnel's avatar
Stefan Behnel committed
547

548 549
@memoize
def _list_pyregr_data_files(test_directory):
550 551 552 553
    is_data_file = re.compile('(?:[.](txt|pem|db|html)|^bad.*[.]py)$').search
    return ['__init__.py'] + [
        filename for filename in list_unchanging_dir(test_directory)
        if is_data_file(filename)]
554 555


556 557 558 559 560 561 562 563 564 565
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


566 567
def import_ext(module_name, file_path=None):
    if file_path:
568 569 570 571 572
        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)
573 574 575 576 577 578 579 580 581 582
    else:
        try:
            from importlib import invalidate_caches
        except ImportError:
            pass
        else:
            invalidate_caches()
        return __import__(module_name, globals(), locals(), ['*'])


583 584
class build_ext(_build_ext):
    def build_extension(self, ext):
585 586 587 588 589 590
        try:
            try: # Py2.7+ & Py3.2+
                compiler_obj = self.compiler_obj
            except AttributeError:
                compiler_obj = self.compiler
            if ext.language == 'c++':
591
                compiler_obj.compiler_so.remove('-Wstrict-prototypes')
592 593
            if CCACHE:
                compiler_obj.compiler_so = CCACHE + compiler_obj.compiler_so
594 595
            if getattr(ext, 'openmp', None) and compiler_obj.compiler_type == 'msvc':
                ext.extra_compile_args.append('/openmp')
596 597
        except Exception:
            pass
598
        _build_ext.build_extension(self, ext)
599

600

601
class ErrorWriter(object):
602
    match_error = re.compile(r'(warning:)?(?:.*:)?\s*([-0-9]+)\s*:\s*([-0-9]+)\s*:\s*(.*)').match
603

604
    def __init__(self, encoding=None):
605
        self.output = []
606 607 608 609 610 611
        self.encoding = encoding

    def write(self, value):
        if self.encoding:
            value = value.encode('ISO-8859-1').decode(self.encoding)
        self.output.append(value)
612

613
    def _collect(self):
614
        s = ''.join(self.output)
615 616
        results = {'errors': [], 'warnings': []}
        for line in s.splitlines():
617 618
            match = self.match_error(line)
            if match:
619
                is_warning, line, column, message = match.groups()
620 621 622
                results['warnings' if is_warning else 'errors'].append((int(line), int(column), message.strip()))

        return [["%d:%d: %s" % values for values in sorted(results[key])] for key in ('errors', 'warnings')]
623 624

    def geterrors(self):
625
        return self._collect()[0]
626 627

    def getwarnings(self):
628
        return self._collect()[1]
629 630

    def getall(self):
631 632 633 634 635
        return self._collect()

    def close(self):
        pass  # ignore, only to match file-like interface

636

637
class Stats(object):
638 639
    def __init__(self, top_n=8):
        self.top_n = top_n
640 641
        self.test_counts = defaultdict(int)
        self.test_times = defaultdict(float)
642
        self.top_tests = defaultdict(list)
643

644
    def add_time(self, name, language, metric, t):
645 646
        self.test_counts[metric] += 1
        self.test_times[metric] += t
647
        top = self.top_tests[metric]
648 649
        push = heapq.heappushpop if len(top) >= self.top_n else heapq.heappush
        # min-heap => pop smallest/shortest until longest times remain
650
        push(top, (t, name, language))
651 652

    @contextmanager
653
    def time(self, name, language, metric):
654 655 656
        t = time.time()
        yield
        t = time.time() - t
657
        self.add_time(name, language, metric, t)
658

659 660 661 662 663 664 665 666 667 668
    def update(self, stats):
        # type: (Stats) -> None
        for metric, t in stats.test_times.items():
            self.test_times[metric] += t
            self.test_counts[metric] += stats.test_counts[metric]
            top = self.top_tests[metric]
            for entry in stats.top_tests[metric]:
                push = heapq.heappushpop if len(top) >= self.top_n else heapq.heappush
                push(top, entry)

669
    def print_stats(self, out=sys.stderr):
670 671
        if not self.test_times:
            return
672
        lines = ['Times:\n']
673
        for metric, t in sorted(self.test_times.items(), key=operator.itemgetter(1), reverse=True):
674
            count = self.test_counts[metric]
675 676 677
            top = self.top_tests[metric]
            lines.append("%-12s: %8.2f sec  (%4d, %6.3f / run) - slowest: %s\n" % (
                metric, t, count, t / count,
678
                ', '.join("'{2}:{1}' ({0:.2f}s)".format(*item) for item in heapq.nlargest(self.top_n, top))))
679 680 681
        out.write(''.join(lines))


682
class TestBuilder(object):
gabrieldemarmiesse's avatar
gabrieldemarmiesse committed
683 684
    def __init__(self, rootdir, workdir, selectors, exclude_selectors, options,
                 with_pyregr, languages, test_bugs, language_level,
685
                 common_utility_dir, pythran_dir=None,
686
                 default_mode='run', stats=None,
687 688
                 add_embedded_test=False, add_cython_import=False,
                 add_cpp_locals_extra_tests=False):
689 690
        self.rootdir = rootdir
        self.workdir = workdir
691
        self.selectors = selectors
692
        self.exclude_selectors = exclude_selectors
gabrieldemarmiesse's avatar
gabrieldemarmiesse committed
693 694 695 696
        self.annotate = options.annotate_source
        self.cleanup_workdir = options.cleanup_workdir
        self.cleanup_sharedlibs = options.cleanup_sharedlibs
        self.cleanup_failures = options.cleanup_failures
697
        self.with_pyregr = with_pyregr
gabrieldemarmiesse's avatar
gabrieldemarmiesse committed
698
        self.cython_only = options.cython_only
699
        self.test_selector = re.compile(options.only_pattern).search if options.only_pattern else None
Stefan Behnel's avatar
Stefan Behnel committed
700
        self.languages = languages
701
        self.test_bugs = test_bugs
gabrieldemarmiesse's avatar
gabrieldemarmiesse committed
702
        self.fork = options.fork
703
        self.language_level = language_level
gabrieldemarmiesse's avatar
gabrieldemarmiesse committed
704
        self.test_determinism = options.test_determinism
705
        self.common_utility_dir = common_utility_dir
706
        self.pythran_dir = pythran_dir
707
        self.default_mode = default_mode
708
        self.stats = stats
709
        self.add_embedded_test = add_embedded_test
710
        self.add_cython_import = add_cython_import
711
        self.capture = options.capture
712
        self.add_cpp_locals_extra_tests = add_cpp_locals_extra_tests
713 714 715

    def build_suite(self):
        suite = unittest.TestSuite()
716 717
        filenames = os.listdir(self.rootdir)
        filenames.sort()
Stefan Behnel's avatar
Stefan Behnel committed
718
        # TODO: parallelise I/O with a thread pool for the different directories once we drop Py2 support
719
        for filename in filenames:
720
            path = os.path.join(self.rootdir, filename)
721
            if os.path.isdir(path) and filename != TEST_SUPPORT_DIR:
722 723
                if filename == 'pyregr' and not self.with_pyregr:
                    continue
724 725
                if filename == 'broken' and not self.test_bugs:
                    continue
726
                suite.addTest(
727
                    self.handle_directory(path, filename))
728 729 730
        if (sys.platform not in ['win32'] and self.add_embedded_test
                # the embedding test is currently broken in Py3.8+, except on Linux.
                and (sys.version_info < (3, 8) or sys.platform != 'darwin')):
731
            # Non-Windows makefile.
732
            if [1 for selector in self.selectors if selector("embedded")] \
733
                    and not [1 for selector in self.exclude_selectors if selector("embedded")]:
734
                suite.addTest(unittest.makeSuite(EmbedTest))
735 736
        return suite

737
    def handle_directory(self, path, context):
738 739 740 741
        workdir = os.path.join(self.workdir, context)
        if not os.path.exists(workdir):
            os.makedirs(workdir)

742
        suite = unittest.TestSuite()
Robert Bradshaw's avatar
Robert Bradshaw committed
743
        filenames = list_unchanging_dir(path)
744 745
        filenames.sort()
        for filename in filenames:
746 747 748
            filepath = os.path.join(path, filename)
            module, ext = os.path.splitext(filename)
            if ext not in ('.py', '.pyx', '.srctree'):
749
                continue
750 751
            if filename.startswith('.'):
                continue # certain emacs backup files
752 753 754 755
            if context == 'pyregr':
                tags = defaultdict(list)
            else:
                tags = parse_tags(filepath)
Robert Bradshaw's avatar
Robert Bradshaw committed
756
            fqmodule = "%s.%s" % (context, module)
757
            if not [ 1 for match in self.selectors
Robert Bradshaw's avatar
Robert Bradshaw committed
758
                     if match(fqmodule, tags) ]:
759
                continue
760
            if self.exclude_selectors:
761
                if [1 for match in self.exclude_selectors
Robert Bradshaw's avatar
Robert Bradshaw committed
762
                        if match(fqmodule, tags)]:
763
                    continue
764

765
            mode = self.default_mode
766 767 768 769 770 771
            if tags['mode']:
                mode = tags['mode'][0]
            elif context == 'pyregr':
                mode = 'pyregr'

            if ext == '.srctree':
772 773 774
                if self.cython_only:
                    # EndToEnd tests always execute arbitrary build and test code
                    continue
775
                if 'cpp' not in tags['tag'] or 'cpp' in self.languages:
776 777 778
                    suite.addTest(EndToEndTest(filepath, workdir,
                             self.cleanup_workdir, stats=self.stats,
                             capture=self.capture))
779 780 781 782 783 784
                continue

            # Choose the test suite.
            if mode == 'pyregr':
                if not filename.startswith('test_'):
                    continue
785
                test_class = CythonPyregrTestCase
786
            elif mode == 'run':
787
                if module.startswith("test_"):
Stefan Behnel's avatar
Stefan Behnel committed
788
                    test_class = CythonUnitTestCase
789
                else:
Stefan Behnel's avatar
Stefan Behnel committed
790
                    test_class = CythonRunTestCase
791
            elif mode in ['compile', 'error']:
Stefan Behnel's avatar
Stefan Behnel committed
792
                test_class = CythonCompileTestCase
793 794
            else:
                raise KeyError('Invalid test mode: ' + mode)
795

Stefan Behnel's avatar
Stefan Behnel committed
796
            for test in self.build_tests(test_class, path, workdir,
797
                                         module, filepath, mode == 'error', tags):
Stefan Behnel's avatar
Stefan Behnel committed
798
                suite.addTest(test)
799

800
            if mode == 'run' and ext == '.py' and not self.cython_only and not filename.startswith('test_'):
801
                # additionally test file in real Python
802 803 804 805 806 807
                min_py_ver = [
                    (int(pyver.group(1)), int(pyver.group(2)))
                    for pyver in map(re.compile(r'pure([0-9]+)[.]([0-9]+)').match, tags['tag'])
                    if pyver
                ]
                if not min_py_ver or any(sys.version_info >= min_ver for min_ver in min_py_ver):
808
                    suite.addTest(PureDoctestTestCase(module, filepath, tags, stats=self.stats))
809

810 811
        return suite

812
    def build_tests(self, test_class, path, workdir, module, module_path, expect_errors, tags):
813 814
        warning_errors = 'werror' in tags['tag']
        expect_warnings = 'warnings' in tags['tag']
Vitja Makarov's avatar
Vitja Makarov committed
815

816 817
        extra_directives_list = [{}]

818
        if expect_errors:
819
            if skip_c(tags) and 'cpp' in self.languages:
Robert Bradshaw's avatar
Robert Bradshaw committed
820 821 822
                languages = ['cpp']
            else:
                languages = self.languages[:1]
823 824
        else:
            languages = self.languages
Robert Bradshaw's avatar
Robert Bradshaw committed
825

826
        if 'c' in languages and skip_c(tags):
827 828
            languages = list(languages)
            languages.remove('c')
829
        if 'cpp' in languages and 'no-cpp' in tags['tag']:
Robert Bradshaw's avatar
Robert Bradshaw committed
830 831
            languages = list(languages)
            languages.remove('cpp')
832 833 834
        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})
835 836 837
        if not languages:
            return []

838
        language_levels = [2, 3] if 'all_language_levels' in tags['tag'] else [None]
839

840 841 842
        pythran_dir = self.pythran_dir
        if 'pythran' in tags['tag'] and not pythran_dir and 'cpp' in languages:
            import pythran.config
843 844 845 846
            try:
                pythran_ext = pythran.config.make_extension(python=True)
            except TypeError:  # old pythran version syntax
                pythran_ext = pythran.config.make_extension()
847 848
            pythran_dir = pythran_ext['include_dirs'][0]

849 850
        add_cython_import = self.add_cython_import and module_path.endswith('.py')

851
        preparse_list = tags.get('preparse', ['id'])
852 853
        tests = [ self.build_test(test_class, path, workdir, module, module_path,
                                  tags, language, language_level,
854
                                  expect_errors, expect_warnings, warning_errors, preparse,
855
                                  pythran_dir if language == "cpp" else None,
856 857
                                  add_cython_import=add_cython_import,
                                  extra_directives=extra_directives)
858
                  for language in languages
859 860
                  for preparse in preparse_list
                  for language_level in language_levels
861
                  for extra_directives in extra_directives_list
862
        ]
Stefan Behnel's avatar
Stefan Behnel committed
863 864
        return tests

865
    def build_test(self, test_class, path, workdir, module, module_path, tags, language, language_level,
866 867
                   expect_errors, expect_warnings, warning_errors, preparse, pythran_dir, add_cython_import,
                   extra_directives):
868 869 870 871
        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)
872
        if preparse != 'id':
873 874 875
            workdir += '_%s' % (preparse,)
        if language_level:
            workdir += '_cy%d' % (language_level,)
876 877
        if extra_directives:
            workdir += ('_directives_'+ '_'.join('%s_%s' % (k, v) for k,v in extra_directives.items()))
878
        return test_class(path, workdir, module, module_path, tags,
Stefan Behnel's avatar
Stefan Behnel committed
879
                          language=language,
880
                          preparse=preparse,
Stefan Behnel's avatar
Stefan Behnel committed
881
                          expect_errors=expect_errors,
882
                          expect_warnings=expect_warnings,
Stefan Behnel's avatar
Stefan Behnel committed
883 884 885
                          annotate=self.annotate,
                          cleanup_workdir=self.cleanup_workdir,
                          cleanup_sharedlibs=self.cleanup_sharedlibs,
886
                          cleanup_failures=self.cleanup_failures,
887
                          cython_only=self.cython_only,
888
                          test_selector=self.test_selector,
889
                          fork=self.fork,
890
                          language_level=language_level or self.language_level,
891
                          warning_errors=warning_errors,
892
                          test_determinism=self.test_determinism,
893
                          common_utility_dir=self.common_utility_dir,
894
                          pythran_dir=pythran_dir,
895 896 897
                          stats=self.stats,
                          add_cython_import=add_cython_import,
                          )
Stefan Behnel's avatar
Stefan Behnel committed
898

899

900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915
def skip_c(tags):
    if 'cpp' in tags['tag']:
        return True

    # We don't want to create a distutils key in the
    # dictionary so we check before looping.
    if 'distutils' in tags:
        for option in tags['distutils']:
            splitted = option.split('=')
            if len(splitted) == 2:
                argument, value = splitted
                if argument.strip() == 'language' and value.strip() == 'c++':
                    return True
    return False


916 917 918 919 920 921 922 923 924 925 926 927
def filter_stderr(stderr_bytes):
    """
    Filter annoying warnings from output.
    """
    if b"Command line warning D9025" in stderr_bytes:
        # MSCV: cl : Command line warning D9025 : overriding '/Ox' with '/Od'
        stderr_bytes = b'\n'.join(
            line for line in stderr_bytes.splitlines()
            if b"Command line warning D9025" not in line)
    return stderr_bytes


928 929 930 931 932 933 934 935 936 937 938
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


939
class CythonCompileTestCase(unittest.TestCase):
940
    def __init__(self, test_directory, workdir, module, module_path, tags, language='c', preparse='id',
941
                 expect_errors=False, expect_warnings=False, annotate=False, cleanup_workdir=True,
942
                 cleanup_sharedlibs=True, cleanup_failures=True, cython_only=False, test_selector=None,
943
                 fork=True, language_level=2, warning_errors=False,
944
                 test_determinism=False,
945 946
                 common_utility_dir=None, pythran_dir=None, stats=None, add_cython_import=False,
                 extra_directives={}):
947
        self.test_directory = test_directory
948
        self.tags = tags
949 950
        self.workdir = workdir
        self.module = module
951
        self.module_path = module_path
Stefan Behnel's avatar
Stefan Behnel committed
952
        self.language = language
953 954
        self.preparse = preparse
        self.name = module if self.preparse == "id" else "%s_%s" % (module, preparse)
955
        self.expect_errors = expect_errors
956
        self.expect_warnings = expect_warnings
957
        self.annotate = annotate
958
        self.cleanup_workdir = cleanup_workdir
Dag Sverre Seljebotn's avatar
Dag Sverre Seljebotn committed
959
        self.cleanup_sharedlibs = cleanup_sharedlibs
960
        self.cleanup_failures = cleanup_failures
Stefan Behnel's avatar
Stefan Behnel committed
961
        self.cython_only = cython_only
962
        self.test_selector = test_selector
963
        self.fork = fork
964
        self.language_level = language_level
Vitja Makarov's avatar
Vitja Makarov committed
965
        self.warning_errors = warning_errors
966
        self.test_determinism = test_determinism
967
        self.common_utility_dir = common_utility_dir
968
        self.pythran_dir = pythran_dir
969
        self.stats = stats
970
        self.add_cython_import = add_cython_import
971
        self.extra_directives = extra_directives
972 973 974
        unittest.TestCase.__init__(self)

    def shortDescription(self):
975 976 977 978
        return "compiling (%s%s%s) %s" % (
            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 "",
979
            self.description_name()
980 981 982 983
        )

    def description_name(self):
        return self.name
984

Stefan Behnel's avatar
Stefan Behnel committed
985
    def setUp(self):
Vitja Makarov's avatar
Vitja Makarov committed
986
        from Cython.Compiler import Options
987 988
        self._saved_options = [
            (name, getattr(Options, name))
989 990 991 992 993
            for name in (
                'warning_errors',
                'clear_to_none',
                'error_on_unknown_names',
                'error_on_uninitialized',
994
                # 'cache_builtins',  # not currently supported due to incorrect global caching
995
            )
996
        ]
997
        self._saved_default_directives = list(Options.get_directive_defaults().items())
Vitja Makarov's avatar
Vitja Makarov committed
998
        Options.warning_errors = self.warning_errors
999
        if sys.version_info >= (3, 4):
1000
            Options._directive_defaults['autotestdict'] = False
1001
        Options._directive_defaults.update(self.extra_directives)
Vitja Makarov's avatar
Vitja Makarov committed
1002

1003 1004
        if not os.path.exists(self.workdir):
            os.makedirs(self.workdir)
Stefan Behnel's avatar
Stefan Behnel committed
1005 1006 1007
        if self.workdir not in sys.path:
            sys.path.insert(0, self.workdir)

1008 1009 1010 1011 1012 1013 1014 1015 1016
        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)

1017
    def tearDown(self):
Vitja Makarov's avatar
Vitja Makarov committed
1018
        from Cython.Compiler import Options
1019 1020
        for name, value in self._saved_options:
            setattr(Options, name, value)
1021
        Options._directive_defaults = dict(self._saved_default_directives)
1022
        unpatch_inspect_isfunction()
Vitja Makarov's avatar
Vitja Makarov committed
1023

Stefan Behnel's avatar
Stefan Behnel committed
1024 1025 1026 1027 1028 1029 1030 1031
        try:
            sys.path.remove(self.workdir)
        except ValueError:
            pass
        try:
            del sys.modules[self.module]
        except KeyError:
            pass
1032 1033 1034 1035 1036 1037 1038

        # 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]

1039 1040 1041
        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
1042 1043
        is_cygwin = sys.platform == 'cygwin'

1044
        if os.path.exists(self.workdir):
1045
            if cleanup_c_files and cleanup_lib_files and not is_cygwin:
1046 1047 1048
                shutil.rmtree(self.workdir, ignore_errors=True)
            else:
                for rmfile in os.listdir(self.workdir):
1049
                    ext = os.path.splitext(rmfile)[1]
1050
                    if not cleanup_c_files:
1051 1052 1053 1054 1055 1056
                        # 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):
1057
                            continue
1058

1059
                    is_shared_obj = ext in (".so", ".dll")
1060 1061

                    if not cleanup_lib_files and is_shared_obj:
1062
                        continue
1063

1064 1065 1066 1067
                    try:
                        rmfile = os.path.join(self.workdir, rmfile)
                        if os.path.isdir(rmfile):
                            shutil.rmtree(rmfile, ignore_errors=True)
1068 1069 1070
                        elif is_cygwin and is_shared_obj:
                            # Delete later
                            _to_clean.append(rmfile)
1071 1072 1073 1074
                        else:
                            os.remove(rmfile)
                    except IOError:
                        pass
1075

1076 1077 1078 1079
                if cleanup_c_files and cleanup_lib_files and is_cygwin:
                    # Finally, remove the work dir itself
                    _to_clean.append(self.workdir)

1080 1081 1082 1083
        if cleanup_c_files and os.path.exists(self.workdir + '-again'):
            shutil.rmtree(self.workdir + '-again', ignore_errors=True)


1084
    def runTest(self):
1085
        self.success = False
1086
        self.runCompileTest()
1087
        self.success = True
1088 1089

    def runCompileTest(self):
1090
        return self.compile(
1091 1092 1093
            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)
1094

1095 1096 1097 1098 1099
    def find_module_source_file(self, source_file):
        if not os.path.exists(source_file):
            source_file = source_file[:-1]
        return source_file

Stefan Behnel's avatar
Stefan Behnel committed
1100 1101 1102
    def build_target_filename(self, module_name):
        target = '%s.%s' % (module_name, self.language)
        return target
1103

Robert Bradshaw's avatar
Robert Bradshaw committed
1104
    def related_files(self, test_directory, module_name):
1105
        is_related = re.compile('%s_.*[.].*' % module_name).match
Robert Bradshaw's avatar
Robert Bradshaw committed
1106
        return [filename for filename in list_unchanging_dir(test_directory)
1107
                if is_related(filename)]
Robert Bradshaw's avatar
Robert Bradshaw committed
1108 1109

    def copy_files(self, test_directory, target_directory, file_list):
1110 1111 1112
        if self.preparse and self.preparse != 'id':
            preparse_func = globals()[self.preparse]
            def copy(src, dest):
1113 1114 1115
                with open(src) as fin:
                    with open(dest, 'w') as fout:
                        fout.write(preparse_func(fin.read()))
1116 1117
        else:
            # use symlink on Unix, copy on Windows
1118
            copy = os.symlink if CAN_SYMLINK else shutil.copy
1119 1120

        join = os.path.join
Robert Bradshaw's avatar
Robert Bradshaw committed
1121
        for filename in file_list:
1122
            file_path = join(test_directory, filename)
1123
            if os.path.exists(file_path):
1124
                copy(file_path, join(target_directory, filename))
1125

Robert Bradshaw's avatar
Robert Bradshaw committed
1126 1127 1128
    def source_files(self, workdir, module_name, file_list):
        return ([self.build_target_filename(module_name)] +
            [filename for filename in file_list
1129
             if not os.path.isfile(os.path.join(workdir, filename))])
1130

1131
    def split_source_and_output(self, source_file, workdir, add_cython_import=False):
1132 1133 1134 1135 1136 1137
        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)

1138 1139
        with io_open(source_file, 'r', encoding='ISO-8859-1') as source_and_output:
            error_writer = warnings_writer = None
1140
            out = io_open(os.path.join(workdir, os.path.basename(source_file)),
1141
                          'w', encoding='ISO-8859-1')
1142 1143
            try:
                for line in source_and_output:
1144
                    if line.startswith(u"_ERRORS"):
1145
                        out.close()
1146
                        out = error_writer = ErrorWriter(encoding=encoding)
1147
                    elif line.startswith(u"_WARNINGS"):
1148
                        out.close()
1149
                        out = warnings_writer = ErrorWriter(encoding=encoding)
1150
                    else:
1151 1152 1153 1154 1155 1156
                        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
1157 1158 1159
                        out.write(line)
            finally:
                out.close()
1160

1161 1162
        return (error_writer.geterrors() if error_writer else [],
                warnings_writer.geterrors() if warnings_writer else [])
1163

1164
    def run_cython(self, test_directory, module, module_path, targetdir, incdir, annotate,
1165
                   extra_compile_options=None):
1166
        include_dirs = INCLUDE_DIRS + [os.path.join(test_directory, '..', TEST_SUPPORT_DIR)]
1167 1168
        if incdir:
            include_dirs.append(incdir)
1169

1170 1171 1172 1173
        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)
Stefan Behnel's avatar
Stefan Behnel committed
1174
        target = os.path.join(targetdir, self.build_target_filename(module))
1175

1176 1177
        if extra_compile_options is None:
            extra_compile_options = {}
1178

1179 1180 1181 1182
        if 'allow_unknown_names' in self.tags['tag']:
            from Cython.Compiler import Options
            Options.error_on_unknown_names = False

1183 1184 1185
        try:
            CompilationOptions
        except NameError:
1186
            from Cython.Compiler.Options import CompilationOptions
1187
            from Cython.Compiler.Main import compile as cython_compile
1188
            from Cython.Compiler.Options import default_options
1189
        common_utility_include_dir = self.common_utility_dir
1190

1191
        options = CompilationOptions(
1192
            default_options,
1193 1194
            include_path = include_dirs,
            output_file = target,
1195
            annotate = annotate,
Stefan Behnel's avatar
Stefan Behnel committed
1196 1197
            use_listing_file = False,
            cplus = self.language == 'cpp',
1198
            np_pythran = self.pythran_dir is not None,
1199
            language_level = self.language_level,
1200
            generate_pxi = False,
1201
            evaluate_tree_assertions = True,
1202
            common_utility_include_dir = common_utility_include_dir,
1203
            **extra_compile_options
1204
            )
1205
        cython_compile(module_path, options=options, full_module_name=module)
1206

1207
    def run_distutils(self, test_directory, module, workdir, incdir,
1208
                      extra_extension_args=None):
1209 1210 1211
        cwd = os.getcwd()
        os.chdir(workdir)
        try:
1212
            build_extension = build_ext(get_distutils_distro())
1213 1214 1215 1216
            build_extension.include_dirs = INCLUDE_DIRS[:]
            if incdir:
                build_extension.include_dirs.append(incdir)
            build_extension.finalize_options()
1217 1218
            if COMPILER:
                build_extension.compiler = COMPILER
1219

1220
            ext_compile_flags = CFLAGS[:]
1221
            ext_compile_defines = CDEFS[:]
1222

1223 1224
            if  build_extension.compiler == 'mingw32':
                ext_compile_flags.append('-Wno-format')
1225 1226
            if extra_extension_args is None:
                extra_extension_args = {}
1227

Robert Bradshaw's avatar
Robert Bradshaw committed
1228 1229
            related_files = self.related_files(test_directory, module)
            self.copy_files(test_directory, workdir, related_files)
1230 1231

            from distutils.core import Extension
1232 1233
            extension = Extension(
                module,
1234 1235
                sources=self.source_files(workdir, module, related_files),
                extra_compile_args=ext_compile_flags,
1236
                define_macros=ext_compile_defines,
1237
                **extra_extension_args
1238
                )
Mark Florisson's avatar
Mark Florisson committed
1239 1240 1241 1242

            if self.language == 'cpp':
                # Set the language now as the fixer might need it
                extension.language = 'c++'
1243 1244 1245 1246
                if self.extra_directives.get('cpp_locals'):
                    extension = update_cpp17_extension(extension)
                    if extension is EXCLUDE_EXT:
                        return
Mark Florisson's avatar
Mark Florisson committed
1247

1248 1249
            if 'distutils' in self.tags:
                from Cython.Build.Dependencies import DistutilsInfo
1250
                from Cython.Utils import open_source_file
1251
                pyx_path = os.path.join(self.test_directory, self.module + ".pyx")
1252 1253
                with open_source_file(pyx_path) as f:
                    DistutilsInfo(f).apply(extension)
1254

1255 1256 1257 1258
            if self.pythran_dir:
                from Cython.Build.Dependencies import update_pythran_extension
                update_pythran_extension(extension)

1259 1260 1261 1262 1263
            # Compile with -DCYTHON_CLINE_IN_TRACEBACK=1 unless we have
            # the "traceback" tag
            if 'traceback' not in self.tags['tag']:
                extension.define_macros.append(("CYTHON_CLINE_IN_TRACEBACK", 1))

Stefan Behnel's avatar
Stefan Behnel committed
1264
            for matcher, fixer in list(EXT_EXTRAS.items()):
1265
                if isinstance(matcher, str):
Stefan Behnel's avatar
Stefan Behnel committed
1266
                    # lazy init
1267 1268 1269
                    del EXT_EXTRAS[matcher]
                    matcher = string_selector(matcher)
                    EXT_EXTRAS[matcher] = fixer
1270
                if matcher(module, self.tags):
Mark Florisson's avatar
Mark Florisson committed
1271 1272
                    newext = fixer(extension)
                    if newext is EXCLUDE_EXT:
1273
                        return skip_test("Test '%s' excluded due to tags '%s'" % (
1274
                            self.name, ', '.join(self.tags.get('tag', ''))))
Mark Florisson's avatar
Mark Florisson committed
1275
                    extension = newext or extension
1276 1277
            if self.language == 'cpp':
                extension.language = 'c++'
1278 1279
            if IS_PY2:
                workdir = str(workdir)  # work around type check in distutils that disallows unicode strings
1280

1281 1282 1283
            build_extension.extensions = [extension]
            build_extension.build_temp = workdir
            build_extension.build_lib  = workdir
1284 1285 1286 1287 1288 1289 1290 1291 1292

            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:
1293
                    error = str(exc)
1294
            stderr = get_stderr()
1295
            if stderr:
1296 1297 1298 1299
                # The test module name should always be ASCII, but let's not risk encoding failures.
                output = b"Compiler output for module %s:\n%s\n" % (module.encode('utf-8'), stderr)
                out = sys.stdout if sys.version_info[0] == 2 else sys.stdout.buffer
                out.write(output)
1300
            if error is not None:
1301
                raise CompileError(u"%s\nCompiler output:\n%s" % (error, prepare_captured(stderr)))
1302 1303
        finally:
            os.chdir(cwd)
1304

1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321
        try:
            get_ext_fullpath = build_extension.get_ext_fullpath
        except AttributeError:
            def get_ext_fullpath(ext_name, self=build_extension):
                # copied from distutils.command.build_ext (missing in Py2.[45])
                fullname = self.get_ext_fullname(ext_name)
                modpath = fullname.split('.')
                filename = self.get_ext_filename(modpath[-1])
                if not self.inplace:
                    filename = os.path.join(*modpath[:-1]+[filename])
                    return os.path.join(self.build_lib, filename)
                package = '.'.join(modpath[0:-1])
                build_py = self.get_finalized_command('build_py')
                package_dir = os.path.abspath(build_py.get_package_dir(package))
                return os.path.join(package_dir, filename)

        return get_ext_fullpath(module)
1322

1323 1324
    def compile(self, test_directory, module, module_path, workdir, incdir,
                expect_errors, expect_warnings, annotate, add_cython_import):
1325
        expected_errors = expected_warnings = errors = warnings = ()
1326
        if expect_errors or expect_warnings or add_cython_import:
1327
            expected_errors, expected_warnings = self.split_source_and_output(
1328
                module_path, workdir, add_cython_import)
1329
            test_directory = workdir
1330
            module_path = os.path.join(workdir, os.path.basename(module_path))
1331

1332 1333 1334 1335
        if WITH_CYTHON:
            old_stderr = sys.stderr
            try:
                sys.stderr = ErrorWriter()
1336
                with self.stats.time(self.name, self.language, 'cython'):
1337
                    self.run_cython(test_directory, module, module_path, workdir, incdir, annotate)
1338
                errors, warnings = sys.stderr.getall()
1339 1340
            finally:
                sys.stderr = old_stderr
1341 1342 1343
            if self.test_determinism and not expect_errors:
                workdir2 = workdir + '-again'
                os.mkdir(workdir2)
1344
                self.run_cython(test_directory, module, module_path, workdir2, incdir, annotate)
1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355
                diffs = []
                for file in os.listdir(workdir2):
                    if (open(os.path.join(workdir, file)).read()
                        != open(os.path.join(workdir2, file)).read()):
                        diffs.append(file)
                        os.system('diff -u %s/%s %s/%s > %s/%s.diff' % (
                            workdir, file,
                            workdir2, file,
                            workdir2, file))
                if diffs:
                    self.fail('Nondeterministic file generation: %s' % ', '.join(diffs))
1356

1357
        tostderr = sys.__stderr__.write
1358 1359
        if expected_warnings or (expect_warnings and warnings):
            self._match_output(expected_warnings, warnings, tostderr)
1360
        if 'cerror' in self.tags['tag']:
1361
            if errors:
1362 1363 1364 1365
                tostderr("\n=== Expected C compile error ===\n")
                tostderr("\n=== Got Cython errors: ===\n")
                tostderr('\n'.join(errors))
                tostderr('\n\n')
1366 1367
                raise RuntimeError('should have generated extension code')
        elif errors or expected_errors:
1368
            self._match_output(expected_errors, errors, tostderr)
1369 1370
            return None

1371 1372
        so_path = None
        if not self.cython_only:
1373
            from Cython.Utils import captured_fd, print_bytes
1374
            from distutils.errors import CompileError, LinkError
1375 1376
            show_output = True
            get_stderr = get_stdout = None
1377
            try:
1378 1379
                with captured_fd(1) as get_stdout:
                    with captured_fd(2) as get_stderr:
1380
                        with self.stats.time(self.name, self.language, 'compile-%s' % self.language):
1381
                            so_path = self.run_distutils(test_directory, module, workdir, incdir)
1382
            except Exception as exc:
1383
                if ('cerror' in self.tags['tag'] and
1384
                    ((get_stderr and get_stderr()) or
1385
                     isinstance(exc, (CompileError, LinkError)))):
1386
                    show_output = False  # expected C compiler failure
1387 1388 1389
                else:
                    raise
            else:
1390
                if 'cerror' in self.tags['tag']:
1391
                    raise RuntimeError('should have failed C compile')
1392 1393 1394
            finally:
                if show_output:
                    stdout = get_stdout and get_stdout().strip()
1395 1396 1397 1398
                    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
1399
                    if stdout:
1400 1401 1402
                        print_bytes(
                            stdout, header_text="\n=== C/C++ compiler output: =========\n",
                            end=None, file=sys.__stderr__)
1403
                    if stderr:
1404 1405 1406
                        print_bytes(
                            stderr, header_text="\n=== C/C++ compiler error output: ===\n",
                            end=None, file=sys.__stderr__)
1407
                    if stdout or stderr:
1408
                        tostderr("\n====================================\n")
1409
        return so_path
1410

1411 1412 1413
    def _match_output(self, expected_output, actual_output, write):
        try:
            for expected, actual in zip(expected_output, actual_output):
1414
                self.assertEqual(expected, actual)
1415 1416
            if len(actual_output) < len(expected_output):
                expected = expected_output[len(actual_output)]
1417
                self.assertEqual(expected, None)
1418 1419
            elif len(actual_output) > len(expected_output):
                unexpected = actual_output[len(expected_output)]
1420
                self.assertEqual(None, unexpected)
1421 1422 1423 1424 1425 1426 1427 1428
        except AssertionError:
            write("\n=== Expected: ===\n")
            write('\n'.join(expected_output))
            write("\n\n=== Got: ===\n")
            write('\n'.join(actual_output))
            write('\n\n')
            raise

1429

1430
class CythonRunTestCase(CythonCompileTestCase):
1431 1432 1433 1434 1435
    def setUp(self):
        CythonCompileTestCase.setUp(self)
        from Cython.Compiler import Options
        Options.clear_to_none = False

1436 1437
    def description_name(self):
        return self.name if self.cython_only else "and running %s" % self.name
1438 1439

    def run(self, result=None):
1440 1441
        if result is None:
            result = self.defaultTestResult()
Stefan Behnel's avatar
Stefan Behnel committed
1442
        result.startTest(self)
1443
        try:
Stefan Behnel's avatar
Stefan Behnel committed
1444
            self.setUp()
1445
            try:
1446
                self.success = False
1447
                ext_so_path = self.runCompileTest()
1448
                failures, errors, skipped = len(result.failures), len(result.errors), len(result.skipped)
1449
                if not self.cython_only and ext_so_path is not None:
1450
                    self.run_tests(result, ext_so_path)
1451 1452 1453
                if failures == len(result.failures) and errors == len(result.errors):
                    # No new errors...
                    self.success = True
1454 1455
            finally:
                check_thread_termination()
1456 1457 1458
        except SkipTest as exc:
            result.addSkip(self, str(exc))
            result.stopTest(self)
1459 1460 1461
        except Exception:
            result.addError(self, sys.exc_info())
            result.stopTest(self)
1462 1463 1464 1465
        try:
            self.tearDown()
        except Exception:
            pass
1466

1467
    def run_tests(self, result, ext_so_path):
1468
        self.run_doctests(self.module, result, ext_so_path)
1469

Stefan Behnel's avatar
Stefan Behnel committed
1470
    def run_doctests(self, module_or_name, result, ext_so_path):
1471
        def run_test(result):
Stefan Behnel's avatar
Stefan Behnel committed
1472
            if isinstance(module_or_name, basestring):
1473
                with self.stats.time(self.name, self.language, 'import'):
1474
                    module = import_ext(module_or_name, ext_so_path)
Stefan Behnel's avatar
Stefan Behnel committed
1475 1476
            else:
                module = module_or_name
1477
            tests = doctest.DocTestSuite(module)
1478 1479
            if self.test_selector:
                filter_test_suite(tests, self.test_selector)
1480
            with self.stats.time(self.name, self.language, 'run'):
1481
                tests.run(result)
1482 1483
        run_forked_test(result, run_test, self.shortDescription(), self.fork)

1484

1485
def run_forked_test(result, run_func, test_name, fork=True):
Stefan Behnel's avatar
Stefan Behnel committed
1486 1487
    if not fork or sys.version_info[0] >= 3 or not hasattr(os, 'fork'):
        run_func(result)
1488 1489
        sys.stdout.flush()
        sys.stderr.flush()
1490 1491 1492 1493 1494 1495 1496 1497 1498
        gc.collect()
        return

    # fork to make sure we do not keep the tested module loaded
    result_handle, result_file = tempfile.mkstemp()
    os.close(result_handle)
    child_id = os.fork()
    if not child_id:
        result_code = 0
1499
        try:
1500
            try:
1501
                tests = partial_result = None
1502
                try:
1503 1504
                    partial_result = PartialTestResult(result)
                    run_func(partial_result)
1505 1506
                    sys.stdout.flush()
                    sys.stderr.flush()
1507 1508 1509
                    gc.collect()
                except Exception:
                    result_code = 1
1510 1511 1512 1513 1514 1515 1516 1517
                    if partial_result is not None:
                        if tests is None:
                            # importing failed, try to fake a test class
                            tests = _FakeClass(
                                failureException=sys.exc_info()[1],
                                _shortDescription=test_name,
                                module_name=None)
                        partial_result.addError(tests, sys.exc_info())
1518 1519 1520
                if partial_result is not None:
                    with open(result_file, 'wb') as output:
                        pickle.dump(partial_result.data(), output)
1521 1522
            except:
                traceback.print_exc()
1523
        finally:
1524
            try: sys.stderr.flush()
1525
            except: pass
1526 1527
            try: sys.stdout.flush()
            except: pass
1528 1529 1530 1531
            os._exit(result_code)

    try:
        cid, result_code = os.waitpid(child_id, 0)
Mark Florisson's avatar
Mark Florisson committed
1532
        module_name = test_name.split()[-1]
1533 1534 1535 1536
        # os.waitpid returns the child's result code in the
        # upper byte of result_code, and the signal it was
        # killed by in the lower byte
        if result_code & 255:
1537 1538 1539
            raise Exception(
                "Tests in module '%s' were unexpectedly killed by signal %d, see test output for details." % (
                    module_name, result_code & 255))
1540
        result_code >>= 8
1541 1542
        if result_code in (0,1):
            try:
1543 1544 1545 1546 1547 1548
                with open(result_file, 'rb') as f:
                    PartialTestResult.join_results(result, pickle.load(f))
            except Exception:
                raise Exception(
                    "Failed to load test result from test in module '%s' after exit status %d,"
                    " see test output for details." % (module_name, result_code))
1549
        if result_code:
1550 1551 1552
            raise Exception(
                "Tests in module '%s' exited with status %d, see test output for details." % (
                    module_name, result_code))
1553
    finally:
1554 1555 1556 1557
        try:
            os.unlink(result_file)
        except:
            pass
1558

1559

1560
class PureDoctestTestCase(unittest.TestCase):
1561
    def __init__(self, module_name, module_path, tags, stats=None):
1562
        self.tags = tags
1563
        self.module_name = self.name = module_name
1564
        self.module_path = module_path
1565
        self.stats = stats
1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578
        unittest.TestCase.__init__(self, 'run')

    def shortDescription(self):
        return "running pure doctests in %s" % self.module_name

    def run(self, result=None):
        if result is None:
            result = self.defaultTestResult()
        loaded_module_name = 'pure_doctest__' + self.module_name
        result.startTest(self)
        try:
            self.setUp()

1579
            with self.stats.time(self.name, 'py', 'pyimport'):
1580 1581 1582 1583 1584 1585
                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)

1586
            try:
1587
                with self.stats.time(self.name, 'py', 'pyrun'):
1588
                    doctest.DocTestSuite(m).run(result)
1589 1590 1591 1592
            finally:
                del m
                if loaded_module_name in sys.modules:
                    del sys.modules[loaded_module_name]
1593
                check_thread_termination()
1594 1595 1596 1597 1598 1599 1600
        except Exception:
            result.addError(self, sys.exc_info())
            result.stopTest(self)
        try:
            self.tearDown()
        except Exception:
            pass
1601

1602 1603 1604 1605 1606 1607
        if 'mypy' in self.tags['tag']:
            try:
                from mypy import api as mypy_api
            except ImportError:
                pass
            else:
1608
                with self.stats.time(self.name, 'py', 'mypy'):
1609
                    mypy_result = mypy_api.run([
1610 1611 1612
                        self.module_path,
                        '--ignore-missing-imports',
                        '--follow-imports', 'skip',
1613
                    ])
1614 1615
                if mypy_result[2]:
                    self.fail(mypy_result[0])
1616 1617


1618 1619 1620 1621
is_private_field = re.compile('^_[^_]').match

class _FakeClass(object):
    def __init__(self, **kwargs):
1622
        self._shortDescription = kwargs.get('module_name')
1623
        self.__dict__.update(kwargs)
1624 1625
    def shortDescription(self):
        return self._shortDescription
1626

1627 1628 1629 1630 1631 1632
try: # Py2.7+ and Py3.2+
    from unittest.runner import _TextTestResult
except ImportError:
    from unittest import _TextTestResult

class PartialTestResult(_TextTestResult):
1633
    def __init__(self, base_result):
1634
        _TextTestResult.__init__(
1635 1636 1637
            self, self._StringIO(), True,
            base_result.dots + base_result.showAll*2)

1638 1639 1640 1641 1642 1643
    def strip_error_results(self, results):
        for test_case, error in results:
            for attr_name in filter(is_private_field, dir(test_case)):
                if attr_name == '_dt_test':
                    test_case._dt_test = _FakeClass(
                        name=test_case._dt_test.name)
Craig Citro's avatar
Craig Citro committed
1644
                elif attr_name != '_shortDescription':
1645 1646
                    setattr(test_case, attr_name, None)

1647
    def data(self):
1648 1649
        self.strip_error_results(self.failures)
        self.strip_error_results(self.errors)
1650
        return (self.failures, self.errors, self.skipped, self.testsRun,
1651 1652 1653 1654 1655 1656
                self.stream.getvalue())

    def join_results(result, data):
        """Static method for merging the result back into the main
        result object.
        """
1657
        failures, errors, skipped, tests_run, output = data
1658 1659 1660
        if output:
            result.stream.write(output)
        result.errors.extend(errors)
1661
        result.skipped.extend(skipped)
1662 1663 1664 1665 1666 1667 1668 1669 1670 1671
        result.failures.extend(failures)
        result.testsRun += tests_run

    join_results = staticmethod(join_results)

    class _StringIO(StringIO):
        def writeln(self, line):
            self.write("%s\n" % line)


1672
class CythonUnitTestCase(CythonRunTestCase):
1673
    def shortDescription(self):
1674
        return "compiling (%s) tests in %s" % (self.language, self.description_name())
1675

1676
    def run_tests(self, result, ext_so_path):
1677
        with self.stats.time(self.name, self.language, 'import'):
1678
            module = import_ext(self.module, ext_so_path)
1679
        tests = unittest.defaultTestLoader.loadTestsFromModule(module)
1680 1681
        if self.test_selector:
            filter_test_suite(tests, self.test_selector)
1682 1683
        with self.stats.time(self.name, self.language, 'run'):
            tests.run(result)
1684 1685 1686


class CythonPyregrTestCase(CythonRunTestCase):
1687 1688 1689 1690
    def setUp(self):
        CythonRunTestCase.setUp(self)
        from Cython.Compiler import Options
        Options.error_on_unknown_names = False
1691
        Options.error_on_uninitialized = False
1692
        Options._directive_defaults.update(dict(
1693 1694
            binding=True, always_allow_keywords=True,
            set_initial_path="SOURCEFILE"))
1695
        patch_inspect_isfunction()
1696

1697 1698 1699
    def related_files(self, test_directory, module_name):
        return _list_pyregr_data_files(test_directory)

1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713
    def _run_unittest(self, result, *classes):
        """Run tests from unittest.TestCase-derived classes."""
        valid_types = (unittest.TestSuite, unittest.TestCase)
        suite = unittest.TestSuite()
        for cls in classes:
            if isinstance(cls, str):
                if cls in sys.modules:
                    suite.addTest(unittest.findTestCases(sys.modules[cls]))
                else:
                    raise ValueError("str arguments must be keys in sys.modules")
            elif isinstance(cls, valid_types):
                suite.addTest(cls)
            else:
                suite.addTest(unittest.makeSuite(cls))
1714
        with self.stats.time(self.name, self.language, 'run'):
1715
            suite.run(result)
1716 1717

    def _run_doctest(self, result, module):
1718
        self.run_doctests(module, result, None)
1719

1720
    def run_tests(self, result, ext_so_path):
1721
        try:
1722
            from test import support
Vitja Makarov's avatar
Vitja Makarov committed
1723 1724
        except ImportError: # Python2.x
            from test import test_support as support
1725

1726 1727 1728 1729 1730
        def run_test(result):
            def run_unittest(*classes):
                return self._run_unittest(result, *classes)
            def run_doctest(module, verbosity=None):
                return self._run_doctest(result, module)
1731

1732
            backup = (support.run_unittest, support.run_doctest)
1733 1734
            support.run_unittest = run_unittest
            support.run_doctest = run_doctest
1735

1736
            try:
1737 1738
                try:
                    sys.stdout.flush() # helps in case of crashes
1739 1740
                    with self.stats.time(self.name, self.language, 'import'):
                        module = import_ext(self.module, ext_so_path)
1741 1742
                    sys.stdout.flush() # helps in case of crashes
                    if hasattr(module, 'test_main'):
1743 1744 1745 1746 1747 1748 1749 1750 1751
                        # help 'doctest.DocFileTest' find the module path through frame inspection
                        fake_caller_module_globals = {
                            'module': module,
                            '__name__': module.__name__,
                        }
                        call_tests = eval(
                            'lambda: module.test_main()',
                            fake_caller_module_globals, fake_caller_module_globals)
                        call_tests()
1752 1753 1754 1755 1756
                        sys.stdout.flush() # helps in case of crashes
                except (unittest.SkipTest, support.ResourceDenied):
                    result.addSkip(self, 'ok')
            finally:
                support.run_unittest, support.run_doctest = backup
1757 1758

        run_forked_test(result, run_test, self.shortDescription(), self.fork)
1759

Stefan Behnel's avatar
Stefan Behnel committed
1760

1761 1762 1763 1764 1765 1766 1767 1768
class TestCodeFormat(unittest.TestCase):

    def __init__(self, cython_dir):
        self.cython_dir = cython_dir
        unittest.TestCase.__init__(self)

    def runTest(self):
        import pycodestyle
1769
        config_file = os.path.join(self.cython_dir, "setup.cfg")
1770
        if not os.path.exists(config_file):
1771
            config_file = os.path.join(os.path.dirname(__file__), "setup.cfg")
1772 1773 1774
        paths = []
        for codedir in ['Cython', 'Demos', 'docs', 'pyximport', 'tests']:
            paths += glob.glob(os.path.join(self.cython_dir, codedir + "/**/*.py"), recursive=True)
1775 1776 1777 1778 1779 1780
        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.")


1781
include_debugger = IS_CPYTHON
1782

Stefan Behnel's avatar
Stefan Behnel committed
1783

1784
def collect_unittests(path, module_prefix, suite, selectors, exclude_selectors):
1785 1786 1787 1788 1789 1790 1791
    def file_matches(filename):
        return filename.startswith("Test") and filename.endswith(".py")

    def package_matches(dirname):
        return dirname == "Tests"

    loader = unittest.TestLoader()
1792

1793 1794 1795
    if include_debugger:
        skipped_dirs = []
    else:
1796
        skipped_dirs = ['Cython' + os.path.sep + 'Debugger' + os.path.sep]
1797

1798
    for dirpath, dirnames, filenames in os.walk(path):
1799 1800 1801 1802 1803 1804 1805 1806 1807
        if dirpath != path and "__init__.py" not in filenames:
            skipped_dirs.append(dirpath + os.path.sep)
            continue
        skip = False
        for dir in skipped_dirs:
            if dirpath.startswith(dir):
                skip = True
        if skip:
            continue
1808 1809 1810 1811 1812
        parentname = os.path.split(dirpath)[-1]
        if package_matches(parentname):
            for f in filenames:
                if file_matches(f):
                    filepath = os.path.join(dirpath, f)[:-len(".py")]
1813
                    modulename = module_prefix + filepath[len(path)+1:].replace(os.path.sep, '.')
Stefan Behnel's avatar
Stefan Behnel committed
1814
                    if not any(1 for match in selectors if match(modulename)):
1815
                        continue
Stefan Behnel's avatar
Stefan Behnel committed
1816
                    if any(1 for match in exclude_selectors if match(modulename)):
1817
                        continue
1818 1819 1820
                    module = __import__(modulename)
                    for x in modulename.split('.')[1:]:
                        module = getattr(module, x)
Robert Bradshaw's avatar
Robert Bradshaw committed
1821
                    suite.addTests([loader.loadTestsFromModule(module)])
1822

1823

1824
def collect_doctests(path, module_prefix, suite, selectors, exclude_selectors):
1825
    def package_matches(dirname):
1826 1827
        if dirname == 'Debugger' and not include_debugger:
            return False
1828
        return dirname not in ("Mac", "Distutils", "Plex", "Tempita")
1829
    def file_matches(filename):
Mark Florisson's avatar
Tests!  
Mark Florisson committed
1830
        filename, ext = os.path.splitext(filename)
1831 1832
        excludelist = ['libcython', 'libpython', 'test_libcython_in_gdb',
                       'TestLibCython']
Mark Florisson's avatar
Tests!  
Mark Florisson committed
1833 1834 1835 1836
        return (ext == '.py' and not
                '~' in filename and not
                '#' in filename and not
                filename.startswith('.') and not
1837
                filename in excludelist)
Stefan Behnel's avatar
Stefan Behnel committed
1838
    import doctest
1839
    for dirpath, dirnames, filenames in os.walk(path):
Robert Bradshaw's avatar
Robert Bradshaw committed
1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851
        for dir in list(dirnames):
            if not package_matches(dir):
                dirnames.remove(dir)
        for f in filenames:
            if file_matches(f):
                if not f.endswith('.py'): continue
                filepath = os.path.join(dirpath, f)
                if os.path.getsize(filepath) == 0: continue
                filepath = filepath[:-len(".py")]
                modulename = module_prefix + filepath[len(path)+1:].replace(os.path.sep, '.')
                if not [ 1 for match in selectors if match(modulename) ]:
                    continue
1852 1853
                if [ 1 for match in exclude_selectors if match(modulename) ]:
                    continue
Robert Bradshaw's avatar
Robert Bradshaw committed
1854 1855 1856
                if 'in_gdb' in modulename:
                    # These should only be imported from gdb.
                    continue
Robert Bradshaw's avatar
Robert Bradshaw committed
1857 1858 1859 1860 1861 1862 1863 1864
                module = __import__(modulename)
                for x in modulename.split('.')[1:]:
                    module = getattr(module, x)
                if hasattr(module, "__doc__") or hasattr(module, "__test__"):
                    try:
                        suite.addTest(doctest.DocTestSuite(module))
                    except ValueError: # no tests
                        pass
1865

1866 1867 1868 1869 1870 1871

class EndToEndTest(unittest.TestCase):
    """
    This is a test of build/*.srctree files, where srctree defines a full
    directory structure and its header gives a list of commands to run.
    """
Robert Bradshaw's avatar
Robert Bradshaw committed
1872
    cython_root = os.path.dirname(os.path.abspath(__file__))
1873

1874
    def __init__(self, treefile, workdir, cleanup_workdir=True, stats=None, capture=True):
1875
        self.name = os.path.splitext(os.path.basename(treefile))[0]
1876
        self.treefile = treefile
1877
        self.workdir = os.path.join(workdir, self.name)
1878
        self.cleanup_workdir = cleanup_workdir
1879
        self.stats = stats
1880
        self.capture = capture
1881 1882 1883
        cython_syspath = [self.cython_root]
        for path in sys.path:
            if path.startswith(self.cython_root) and path not in cython_syspath:
1884 1885
                # Py3 installation and refnanny build prepend their
                # fixed paths to sys.path => prefer that over the
1886 1887 1888
                # generic one (cython_root itself goes last)
                cython_syspath.append(path)
        self.cython_syspath = os.pathsep.join(cython_syspath[::-1])
1889 1890 1891
        unittest.TestCase.__init__(self)

    def shortDescription(self):
1892
        return "End-to-end %s" % self.name
1893 1894 1895

    def setUp(self):
        from Cython.TestUtils import unpack_source_tree
1896
        _, self.commands = unpack_source_tree(self.treefile, self.workdir, self.cython_root)
1897 1898 1899 1900 1901
        self.old_dir = os.getcwd()
        os.chdir(self.workdir)

    def tearDown(self):
        if self.cleanup_workdir:
1902 1903 1904 1905 1906 1907 1908
            for trial in range(5):
                try:
                    shutil.rmtree(self.workdir)
                except OSError:
                    time.sleep(0.1)
                else:
                    break
1909
        os.chdir(self.old_dir)
1910

1911 1912 1913 1914 1915 1916
    def _try_decode(self, content):
        try:
            return content.decode()
        except UnicodeDecodeError:
            return content.decode('iso-8859-1')

1917
    def runTest(self):
1918
        self.success = False
1919
        old_path = os.environ.get('PYTHONPATH')
1920
        env = dict(os.environ)
1921 1922
        new_path = self.cython_syspath
        if old_path:
1923
            new_path = new_path + os.pathsep + self.workdir + os.pathsep + old_path
1924
        env['PYTHONPATH'] = new_path
1925 1926
        if not env.get("PYTHONIOENCODING"):
            env["PYTHONIOENCODING"] = sys.stdout.encoding or sys.getdefaultencoding()
1927 1928 1929
        cmd = []
        out = []
        err = []
1930
        for command_no, command in enumerate(self.commands, 1):
1931
            with self.stats.time('%s(%d)' % (self.name, command_no), 'c',
1932
                                 'etoe-build' if 'setup.py' in command else 'etoe-run'):
1933
                if self.capture:
1934
                    p = subprocess.Popen(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE, env=env)
1935 1936 1937
                    _out, _err = p.communicate()
                    res = p.returncode
                else:
1938
                    p = subprocess.call(command, env=env)
1939 1940
                    _out, _err = b'', b''
                    res = p
1941 1942 1943
                cmd.append(command)
                out.append(_out)
                err.append(_err)
1944 1945
            if res == 0 and b'REFNANNY: ' in _out:
                res = -1
1946
            if res != 0:
1947 1948 1949
                for c, o, e in zip(cmd, out, err):
                    sys.stderr.write("%s\n%s\n%s\n\n" % (
                        c, self._try_decode(o), self._try_decode(e)))
1950
            self.assertEqual(0, res, "non-zero exit status")
1951
        self.success = True
1952 1953


1954 1955 1956 1957
# TODO: Support cython_freeze needed here as well.
# TODO: Windows support.

class EmbedTest(unittest.TestCase):
1958

1959
    working_dir = "Demos/embed"
1960

1961 1962 1963
    def setUp(self):
        self.old_dir = os.getcwd()
        os.chdir(self.working_dir)
1964
        os.system(
1965
            "make PYTHON='%s' clean > /dev/null" % sys.executable)
1966

1967 1968
    def tearDown(self):
        try:
1969 1970
            os.system(
                "make PYTHON='%s' clean > /dev/null" % sys.executable)
1971 1972 1973
        except:
            pass
        os.chdir(self.old_dir)
1974

1975
    def test_embed(self):
1976
        libname = sysconfig.get_config_var('LIBRARY')
1977
        libdir = sysconfig.get_config_var('LIBDIR')
1978 1979 1980 1981 1982 1983 1984
        if not os.path.isdir(libdir) or libname not in os.listdir(libdir):
            libdir = os.path.join(os.path.dirname(sys.executable), '..', 'lib')
            if not os.path.isdir(libdir) or libname not in os.listdir(libdir):
                libdir = os.path.join(libdir, 'python%d.%d' % sys.version_info[:2], 'config')
                if not os.path.isdir(libdir) or libname not in os.listdir(libdir):
                    # report the error for the original directory
                    libdir = sysconfig.get_config_var('LIBDIR')
Stefan Behnel's avatar
Stefan Behnel committed
1985
        cython = os.path.abspath(os.path.join('..', '..', 'cython.py'))
1986

1987
        try:
1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999
            subprocess.check_output([
                "make",
                "PYTHON='%s'" % sys.executable,
                "CYTHON='%s'" % cython,
                "LIBDIR1='%s'" % libdir,
                "paths", "test",
            ])
        except subprocess.CalledProcessError as err:
            print(err.output.decode())
            raise
        self.assertTrue(True)  # :)

2000

2001 2002 2003 2004
def load_listfile(filename):
    # just re-use the FileListExclude implementation
    fle = FileListExcluder(filename)
    return list(fle.excludes)
2005 2006

class MissingDependencyExcluder(object):
2007
    def __init__(self, deps):
2008
        # deps: { matcher func : module name }
2009
        self.exclude_matchers = []
2010
        for matcher, module_name in deps.items():
2011
            try:
2012
                module = __import__(module_name)
2013
            except ImportError:
Robert Bradshaw's avatar
Robert Bradshaw committed
2014
                self.exclude_matchers.append(string_selector(matcher))
2015 2016 2017 2018
                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))
2019
        self.tests_missing_deps = []
2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037

    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
            if 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

Robert Bradshaw's avatar
Robert Bradshaw committed
2038
    def __call__(self, testname, tags=None):
2039
        for matcher in self.exclude_matchers:
Robert Bradshaw's avatar
Robert Bradshaw committed
2040
            if matcher(testname, tags):
2041 2042 2043 2044
                self.tests_missing_deps.append(testname)
                return True
        return False

2045 2046

class VersionDependencyExcluder(object):
2047 2048 2049 2050
    def __init__(self, deps):
        # deps: { version : matcher func }
        from sys import version_info
        self.exclude_matchers = []
2051 2052
        for ver, (compare, matcher) in deps.items():
            if compare(version_info, ver):
2053 2054
                self.exclude_matchers.append(matcher)
        self.tests_missing_deps = []
Robert Bradshaw's avatar
Robert Bradshaw committed
2055
    def __call__(self, testname, tags=None):
2056 2057 2058 2059 2060 2061
        for matcher in self.exclude_matchers:
            if matcher(testname):
                self.tests_missing_deps.append(testname)
                return True
        return False

2062

2063
class FileListExcluder(object):
2064 2065
    def __init__(self, list_file, verbose=False):
        self.verbose = verbose
2066
        self.excludes = {}
2067 2068 2069
        self._list_file = os.path.relpath(list_file)
        with open(list_file) as f:
            for line in f:
2070 2071 2072
                line = line.strip()
                if line and line[0] != '#':
                    self.excludes[line.split()[0]] = True
2073

Robert Bradshaw's avatar
Robert Bradshaw committed
2074
    def __call__(self, testname, tags=None):
2075
        exclude = any(string_selector(ex)(testname) for ex in self.excludes)
2076
        if exclude and self.verbose:
2077 2078 2079
            print("Excluding %s because it's listed in %s"
                  % (testname, self._list_file))
        return exclude
2080

2081

2082
class TagsSelector(object):
Robert Bradshaw's avatar
Robert Bradshaw committed
2083 2084 2085
    def __init__(self, tag, value):
        self.tag = tag
        self.value = value
2086

Robert Bradshaw's avatar
Robert Bradshaw committed
2087 2088 2089 2090 2091 2092
    def __call__(self, testname, tags=None):
        if tags is None:
            return False
        else:
            return self.value in tags[self.tag]

2093

2094
class RegExSelector(object):
Robert Bradshaw's avatar
Robert Bradshaw committed
2095
    def __init__(self, pattern_string):
2096
        try:
2097
            self.regex_matches = re.compile(pattern_string, re.I|re.U).search
2098 2099 2100
        except re.error:
            print('Invalid pattern: %r' % pattern_string)
            raise
Robert Bradshaw's avatar
Robert Bradshaw committed
2101 2102

    def __call__(self, testname, tags=None):
2103
        return self.regex_matches(testname)
Robert Bradshaw's avatar
Robert Bradshaw committed
2104

2105

Robert Bradshaw's avatar
Robert Bradshaw committed
2106
def string_selector(s):
2107 2108
    if ':' in s:
        return TagsSelector(*s.split(':', 1))
Robert Bradshaw's avatar
Robert Bradshaw committed
2109
    else:
2110
        return RegExSelector(s)
2111

2112 2113

class ShardExcludeSelector(object):
2114
    # This is an exclude selector so it can override the (include) selectors.
Robert Bradshaw's avatar
Robert Bradshaw committed
2115 2116
    # It may not provide uniform distribution (in time or count), but is a
    # determanistic partition of the tests which is important.
2117 2118 2119 2120

    # Random seed to improve the hash distribution.
    _seed = base64.b64decode(b'2ged1EtsGz/GkisJr22UcLeP6n9XIaA5Vby2wM49Wvg=')

2121 2122 2123 2124
    def __init__(self, shard_num, shard_count):
        self.shard_num = shard_num
        self.shard_count = shard_count

2125
    def __call__(self, testname, tags=None, _hash=zlib.crc32, _is_py2=IS_PY2):
2126
        # Cannot use simple hash() here as shard processes might use different hash seeds.
2127
        # CRC32 is fast and simple, but might return negative values in Py2.
2128
        hashval = _hash(self._seed + testname) & 0x7fffffff if _is_py2 else _hash(self._seed + testname.encode())
2129
        return hashval % self.shard_count != self.shard_num
2130

Robert Bradshaw's avatar
Robert Bradshaw committed
2131

2132 2133
class PendingThreadsError(RuntimeError):
    pass
2134

2135 2136 2137
threads_seen = []

def check_thread_termination(ignore_seen=True):
2138 2139
    if threading is None: # no threading enabled in CPython
        return
2140
    current = threading.current_thread()
2141 2142
    blocking_threads = []
    for t in threading.enumerate():
2143
        if not t.is_alive() or t == current or t.name == 'time_stamper':
2144 2145
            continue
        t.join(timeout=2)
2146
        if t.is_alive():
2147 2148 2149
            if not ignore_seen:
                blocking_threads.append(t)
                continue
2150 2151 2152 2153 2154 2155
            for seen in threads_seen:
                if t is seen:
                    break
            else:
                threads_seen.append(t)
                blocking_threads.append(t)
2156 2157 2158 2159 2160
    if not blocking_threads:
        return
    sys.stderr.write("warning: left-over threads found after running test:\n")
    for t in blocking_threads:
        sys.stderr.write('...%s\n'  % repr(t))
2161
    raise PendingThreadsError("left-over threads found after running test")
2162

2163 2164
def subprocess_output(cmd):
    try:
Mark Florisson's avatar
Mark Florisson committed
2165 2166
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        return p.communicate()[0].decode('UTF-8')
2167 2168 2169 2170 2171 2172 2173 2174
    except OSError:
        return ''

def get_version():
    from Cython.Compiler.Version import version as cython_version
    full_version = cython_version
    top = os.path.dirname(os.path.abspath(__file__))
    if os.path.exists(os.path.join(top, '.git')):
Stefan Behnel's avatar
Stefan Behnel committed
2175
        old_dir = os.getcwd()
2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188
        try:
            os.chdir(top)
            head_commit = subprocess_output(['git', 'rev-parse', 'HEAD']).strip()
            version_commit = subprocess_output(['git', 'rev-parse', cython_version]).strip()
            diff = subprocess_output(['git', 'diff', '--stat']).strip()
            if head_commit != version_commit:
                full_version += " " + head_commit
            if diff:
                full_version += ' + uncommitted changes'
        finally:
            os.chdir(old_dir)
    return full_version

2189 2190 2191 2192 2193 2194 2195 2196
_orig_stdout, _orig_stderr = sys.stdout, sys.stderr
def flush_and_terminate(status):
    try:
        _orig_stdout.flush()
        _orig_stderr.flush()
    finally:
        os._exit(status)

2197
def main():
2198

2199
    global DISTDIR, WITH_CYTHON
2200 2201 2202 2203

    # Set an environment variable to the top directory
    os.environ['CYTHON_PROJECT_DIR'] = os.path.abspath(os.path.dirname(__file__))

2204 2205
    DISTDIR = os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]))

2206 2207 2208 2209 2210 2211 2212 2213
    from Cython.Compiler import DebugFlags
    args = []
    for arg in sys.argv[1:]:
        if arg.startswith('--debug') and arg[2:].replace('-', '_') in dir(DebugFlags):
            setattr(DebugFlags, arg[2:].replace('-', '_'), True)
        else:
            args.append(arg)

2214
    from optparse import OptionParser
2215
    parser = OptionParser(usage="usage: %prog [options] [selector ...]")
2216
    parser.add_option("--no-cleanup", dest="cleanup_workdir",
Stefan Behnel's avatar
Stefan Behnel committed
2217 2218
                      action="store_false", default=True,
                      help="do not delete the generated C files (allows passing --no-cython on next run)")
2219 2220
    parser.add_option("--no-cleanup-sharedlibs", dest="cleanup_sharedlibs",
                      action="store_false", default=True,
Unknown's avatar
Unknown committed
2221
                      help="do not delete the generated shared library files (allows manual module experimentation)")
2222 2223 2224
    parser.add_option("--no-cleanup-failures", dest="cleanup_failures",
                      action="store_false", default=True,
                      help="enable --no-cleanup and --no-cleanup-sharedlibs for failed tests only")
Stefan Behnel's avatar
Stefan Behnel committed
2225 2226 2227
    parser.add_option("--no-cython", dest="with_cython",
                      action="store_false", default=True,
                      help="do not run the Cython compiler, only the C compiler")
2228 2229
    parser.add_option("--compiler", dest="compiler", default=None,
                      help="C compiler type")
2230 2231 2232
    backend_list = ','.join(BACKENDS)
    parser.add_option("--backends", dest="backends", default=backend_list,
                      help="select backends to test (default: %s)" % backend_list)
Stefan Behnel's avatar
Stefan Behnel committed
2233 2234
    parser.add_option("--no-c", dest="use_c",
                      action="store_false", default=True,
2235
                      help="do not test C compilation backend")
Stefan Behnel's avatar
Stefan Behnel committed
2236 2237
    parser.add_option("--no-cpp", dest="use_cpp",
                      action="store_false", default=True,
2238
                      help="do not test C++ compilation backend")
2239 2240 2241
    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")
2242 2243 2244
    parser.add_option("--no-unit", dest="unittests",
                      action="store_false", default=True,
                      help="do not run the unit tests")
2245 2246 2247
    parser.add_option("--no-doctest", dest="doctests",
                      action="store_false", default=True,
                      help="do not run the doctests")
2248 2249 2250
    parser.add_option("--no-file", dest="filetests",
                      action="store_false", default=True,
                      help="do not run the file based tests")
2251 2252
    parser.add_option("--no-pyregr", dest="pyregr",
                      action="store_false", default=True,
2253
                      help="do not run the regression tests of CPython in tests/pyregr/")
2254 2255 2256
    parser.add_option("--no-examples", dest="examples",
                      action="store_false", default=True,
                      help="Do not run the documentation tests in the examples directory.")
2257 2258 2259
    parser.add_option("--no-code-style", dest="code_style",
                      action="store_false", default=True,
                      help="Do not run the code style (PEP8) checks.")
Stefan Behnel's avatar
Stefan Behnel committed
2260
    parser.add_option("--cython-only", dest="cython_only",
2261 2262
                      action="store_true", default=False,
                      help="only compile pyx to c, do not run C compiler or run the tests")
2263
    parser.add_option("--no-refnanny", dest="with_refnanny",
2264
                      action="store_false", default=True,
2265
                      help="do not regression test reference counting")
2266 2267 2268
    parser.add_option("--no-fork", dest="fork",
                      action="store_false", default=True,
                      help="do not fork to run tests")
2269 2270 2271
    parser.add_option("--sys-pyregr", dest="system_pyregr",
                      action="store_true", default=False,
                      help="run the regression tests of the CPython installation")
2272 2273 2274
    parser.add_option("-x", "--exclude", dest="exclude",
                      action="append", metavar="PATTERN",
                      help="exclude tests matching the PATTERN")
2275 2276 2277
    parser.add_option("--listfile", dest="listfile",
                      action="append",
                      help="specify a file containing a list of tests to run")
2278
    parser.add_option("-j", "--shard_count", dest="shard_count", metavar="N",
2279 2280 2281 2282 2283
                      type=int, default=1,
                      help="shard this run into several parallel runs")
    parser.add_option("--shard_num", dest="shard_num", metavar="K",
                      type=int, default=-1,
                      help="test only this single shard")
Stefan Behnel's avatar
Stefan Behnel committed
2284 2285 2286
    parser.add_option("--profile", dest="profile",
                      action="store_true", default=False,
                      help="enable profiling of the tests")
2287
    parser.add_option("-C", "--coverage", dest="coverage",
Stefan Behnel's avatar
Stefan Behnel committed
2288 2289
                      action="store_true", default=False,
                      help="collect source coverage data for the Compiler")
2290 2291 2292
    parser.add_option("--coverage-xml", dest="coverage_xml",
                      action="store_true", default=False,
                      help="collect source coverage data for the Compiler in XML format")
Stefan Behnel's avatar
Stefan Behnel committed
2293 2294 2295
    parser.add_option("--coverage-html", dest="coverage_html",
                      action="store_true", default=False,
                      help="collect source coverage data for the Compiler in HTML format")
Stefan Behnel's avatar
Stefan Behnel committed
2296
    parser.add_option("-A", "--annotate", dest="annotate_source",
2297
                      action="store_true", default=True,
Stefan Behnel's avatar
Stefan Behnel committed
2298
                      help="generate annotated HTML versions of the test source files")
2299 2300 2301
    parser.add_option("--no-annotate", dest="annotate_source",
                      action="store_false",
                      help="do not generate annotated HTML versions of the test source files")
2302
    parser.add_option("-v", "--verbose", dest="verbosity",
Stefan Behnel's avatar
Stefan Behnel committed
2303 2304
                      action="count", default=0,
                      help="display test progress, pass twice to print test names")
2305 2306
    parser.add_option("-T", "--ticket", dest="tickets",
                      action="append",
2307
                      help="a bug ticket number to run the respective test in 'tests/*'")
2308 2309
    parser.add_option("-k", dest="only_pattern",
                      help="a regex pattern for selecting doctests and test functions in the test modules")
2310 2311 2312
    parser.add_option("-3", dest="language_level",
                      action="store_const", const=3, default=2,
                      help="set language level to Python 3 (useful for running the CPython regression tests)'")
2313 2314
    parser.add_option("--xml-output", dest="xml_output_dir", metavar="DIR",
                      help="write test results in XML to directory DIR")
2315 2316 2317
    parser.add_option("--exit-ok", dest="exit_ok", default=False,
                      action="store_true",
                      help="exit without error code even on test failures")
2318 2319 2320
    parser.add_option("--failfast", dest="failfast", default=False,
                      action="store_true",
                      help="stop on first failure or error")
2321
    parser.add_option("--root-dir", dest="root_dir", default=os.path.join(DISTDIR, 'tests'),
2322 2323
                      help=("Directory to look for the file based "
                            "tests (the ones which are deactivated with '--no-file'."))
2324 2325
    parser.add_option("--examples-dir", dest="examples_dir",
                      default=os.path.join(DISTDIR, 'docs', 'examples'),
2326
                      help="Directory to look for documentation example tests")
2327
    parser.add_option("--work-dir", dest="work_dir", default=os.path.join(os.getcwd(), 'TEST_TMP'),
2328
                      help="working directory")
2329 2330
    parser.add_option("--cython-dir", dest="cython_dir", default=os.getcwd(),
                      help="Cython installation directory (default: use local source version)")
2331 2332
    parser.add_option("--debug", dest="for_debugging", default=False, action="store_true",
                      help="configure for easier use with a debugger (e.g. gdb)")
2333 2334
    parser.add_option("--pyximport-py", dest="pyximport_py", default=False, action="store_true",
                      help="use pyximport to automatically compile imported .pyx and .py files")
2335 2336
    parser.add_option("--watermark", dest="watermark", default=None,
                      help="deterministic generated by string")
2337
    parser.add_option("--use_common_utility_dir", default=False, action="store_true")
2338
    parser.add_option("--use_formal_grammar", default=False, action="store_true")
2339 2340
    parser.add_option("--test_determinism", default=False, action="store_true",
                      help="test whether Cython's output is deterministic")
2341 2342
    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")
2343 2344
    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")
2345 2346
    parser.add_option("--limited-api", dest="limited_api", default=False, action="store_true",
                      help="Compiles Cython using CPython's LIMITED_API")
2347

2348
    options, cmd_args = parser.parse_args(args)
2349

2350 2351
    if options.with_cython and sys.version_info[0] >= 3:
        sys.path.insert(0, options.cython_dir)
2352

2353 2354 2355 2356
    # requires glob with the wildcard.
    if sys.version_info < (3, 5) or cmd_args:
        options.code_style = False

2357
    WITH_CYTHON = options.with_cython
2358

2359
    coverage = None
Stefan Behnel's avatar
Stefan Behnel committed
2360
    if options.coverage or options.coverage_xml or options.coverage_html:
2361 2362 2363 2364 2365
        if not WITH_CYTHON:
            options.coverage = options.coverage_xml = options.coverage_html = False
        elif options.shard_num == -1:
            print("Enabling coverage analysis")
            from coverage import coverage as _coverage
2366
            coverage = _coverage(branch=True)
2367 2368
            coverage.erase()
            coverage.start()
2369

2370 2371 2372
    if options.xml_output_dir:
        shutil.rmtree(options.xml_output_dir, ignore_errors=True)

2373 2374 2375 2376
    if options.listfile:
        for listfile in options.listfile:
            cmd_args.extend(load_listfile(listfile))

2377
    if options.capture and not options.for_debugging:
2378 2379 2380
        keep_alive_interval = 10
    else:
        keep_alive_interval = None
2381
    if options.shard_count > 1 and options.shard_num == -1:
2382 2383 2384
        if "PYTHONIOENCODING" not in os.environ:
            # Make sure subprocesses can print() Unicode text.
            os.environ["PYTHONIOENCODING"] = sys.stdout.encoding or sys.getdefaultencoding()
2385 2386 2387
        import multiprocessing
        pool = multiprocessing.Pool(options.shard_count)
        tasks = [(options, cmd_args, shard_num) for shard_num in range(options.shard_count)]
2388
        error_shards = []
2389
        failure_outputs = []
2390
        # NOTE: create process pool before time stamper thread to avoid forking issues.
2391
        total_time = time.time()
2392
        stats = Stats()
2393
        with time_stamper_thread(interval=keep_alive_interval):
2394
            for shard_num, shard_stats, return_code, failure_output in pool.imap_unordered(runtests_callback, tasks):
2395
                if return_code != 0:
2396 2397
                    error_shards.append(shard_num)
                    failure_outputs.append(failure_output)
2398 2399
                    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))
2400
                stats.update(shard_stats)
2401 2402
        pool.close()
        pool.join()
2403
        total_time = time.time() - total_time
2404
        sys.stderr.write("Sharded tests run in %d seconds (%.1f minutes)\n" % (round(total_time), total_time / 60.))
2405 2406 2407 2408
        if error_shards:
            sys.stderr.write("Errors found in shards %s\n" % ", ".join([str(e) for e in error_shards]))
            for failure_output in zip(error_shards, failure_outputs):
                sys.stderr.write("\nErrors from shard %s:\n%s" % failure_output)
2409 2410 2411 2412
            return_code = 1
        else:
            return_code = 0
    else:
2413
        with time_stamper_thread(interval=keep_alive_interval):
2414
            _, stats, return_code, _ = runtests(options, cmd_args, coverage)
2415 2416 2417 2418 2419 2420

    if coverage:
        if options.shard_count > 1 and options.shard_num == -1:
            coverage.combine()
        coverage.stop()

2421
    stats.print_stats(sys.stderr)
2422 2423 2424
    if coverage:
        save_coverage(coverage, options)

2425 2426 2427
    sys.stderr.write("ALL DONE\n")
    sys.stderr.flush()

2428 2429 2430 2431 2432
    try:
        check_thread_termination(ignore_seen=False)
    except PendingThreadsError:
        # normal program exit won't kill the threads, do it the hard way here
        flush_and_terminate(return_code)
Stefan Behnel's avatar
Stefan Behnel committed
2433 2434
    else:
        sys.exit(return_code)
2435 2436


2437 2438 2439 2440 2441 2442
@contextmanager
def time_stamper_thread(interval=10):
    """
    Print regular time stamps into the build logs to find slow tests.
    @param interval: time interval in seconds
    """
2443 2444 2445 2446 2447
    if not interval or interval < 0:
        # Do nothing
        yield
        return

2448 2449 2450 2451 2452 2453
    try:
        _xrange = xrange
    except NameError:
        _xrange = range

    import threading
2454
    import datetime
2455 2456
    from time import sleep

2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478
    interval = _xrange(interval * 4)
    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:
                if stop:
                    return
                sleep(1./4)
            write('\n#### %s\n' % now())

    thread = threading.Thread(target=time_stamper, name='time_stamper')
    thread.setDaemon(True)  # Py2 ...
    thread.start()
    try:
2479
        yield
2480 2481 2482 2483
    finally:
        stop = True
        thread.join()
        os.close(stderr)
2484 2485


2486 2487
def configure_cython(options):
    global CompilationOptions, pyrex_default_options, cython_compile
2488
    from Cython.Compiler.Options import \
2489
        CompilationOptions, \
Stefan Behnel's avatar
Stefan Behnel committed
2490 2491
        default_options as pyrex_default_options
    from Cython.Compiler.Options import _directive_defaults as directive_defaults
2492 2493 2494 2495 2496 2497 2498
    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
    pyrex_default_options['formal_grammar'] = options.use_formal_grammar
Stefan Behnel's avatar
Stefan Behnel committed
2499 2500
    if options.profile:
        directive_defaults['profile'] = True
2501 2502 2503
    if options.watermark:
        import Cython.Compiler.Version
        Cython.Compiler.Version.watermark = options.watermark
2504 2505


2506 2507 2508 2509 2510 2511 2512 2513 2514
def save_coverage(coverage, options):
    if options.coverage:
        coverage.report(show_missing=0)
    if options.coverage_xml:
        coverage.xml_report(outfile="coverage-report.xml")
    if options.coverage_html:
        coverage.html_report(directory="coverage-report-html")


2515 2516 2517 2518 2519
def runtests_callback(args):
    options, cmd_args, shard_num = args
    options.shard_num = shard_num
    return runtests(options, cmd_args)

2520

2521
def runtests(options, cmd_args, coverage=None):
2522
    # faulthandler should be able to provide a limited traceback
davfsa's avatar
davfsa committed
2523
    # in the event of a segmentation fault. Only available on Python 3.3+
2524 2525 2526 2527 2528 2529 2530
    try:
        import faulthandler
    except ImportError:
        pass  # OK - not essential
    else:
        faulthandler.enable()

2531 2532 2533 2534 2535 2536 2537 2538
    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()
2539

2540 2541 2542 2543
    WITH_CYTHON = options.with_cython
    ROOTDIR = os.path.abspath(options.root_dir)
    WORKDIR = os.path.abspath(options.work_dir)

2544 2545 2546
    if WITH_CYTHON:
        configure_cython(options)

2547
    xml_output_dir = options.xml_output_dir
2548 2549
    if options.shard_num > -1:
        WORKDIR = os.path.join(WORKDIR, str(options.shard_num))
2550 2551
        if xml_output_dir:
            xml_output_dir = os.path.join(xml_output_dir, 'shard-%03d' % options.shard_num)
2552

2553
    # RUN ALL TESTS!
2554
    UNITTEST_MODULE = "Cython"
2555
    UNITTEST_ROOT = os.path.join(os.path.dirname(__file__), UNITTEST_MODULE)
2556 2557
    if WITH_CYTHON:
        if os.path.exists(WORKDIR):
2558
            for path in os.listdir(WORKDIR):
2559
                if path in ("support", "Cy3"): continue
2560
                shutil.rmtree(os.path.join(WORKDIR, path), ignore_errors=True)
2561 2562
    if not os.path.exists(WORKDIR):
        os.makedirs(WORKDIR)
2563

2564 2565 2566 2567 2568 2569 2570
    if options.shard_num <= 0:
        sys.stderr.write("Python %s\n" % sys.version)
        sys.stderr.write("\n")
        if WITH_CYTHON:
            sys.stderr.write("Running tests against Cython %s\n" % get_version())
        else:
            sys.stderr.write("Running tests without Cython.\n")
2571

2572 2573 2574 2575
    if options.for_debugging:
        options.cleanup_workdir = False
        options.cleanup_sharedlibs = False
        options.fork = False
2576
        if WITH_CYTHON and include_debugger:
2577
            from Cython.Compiler.Options import default_options as compiler_default_options
2578 2579
            compiler_default_options['gdb_debug'] = True
            compiler_default_options['output_dir'] = os.getcwd()
2580

2581 2582 2583 2584 2585
    if IS_PYPY:
        if options.with_refnanny:
            sys.stderr.write("Disabling refnanny in PyPy\n")
            options.with_refnanny = False

2586 2587 2588 2589 2590
    if options.with_refnanny:
        from pyximport.pyxbuild import pyx_to_dll
        libpath = pyx_to_dll(os.path.join("Cython", "Runtime", "refnanny.pyx"),
                             build_in_temp=True,
                             pyxbuild_dir=os.path.join(WORKDIR, "support"))
2591
        sys.path.insert(0, os.path.split(libpath)[0])
2592
        CDEFS.append(('CYTHON_REFNANNY', '1'))
2593

2594 2595
    if options.limited_api:
        CFLAGS.append("-DCYTHON_LIMITED_API=1")
2596
        CFLAGS.append('-Wno-unused-function')
2597

2598
    if xml_output_dir and options.fork:
Stefan Behnel's avatar
cleanup  
Stefan Behnel committed
2599 2600 2601 2602
        # doesn't currently work together
        sys.stderr.write("Disabling forked testing to support XML test output\n")
        options.fork = False

2603 2604
    if WITH_CYTHON:
        sys.stderr.write("Using Cython language level %d.\n" % options.language_level)
2605

2606
    test_bugs = False
Stefan Behnel's avatar
Stefan Behnel committed
2607 2608 2609
    if options.tickets:
        for ticket_number in options.tickets:
            test_bugs = True
Robert Bradshaw's avatar
Robert Bradshaw committed
2610
            cmd_args.append('ticket:%s' % ticket_number)
2611 2612 2613 2614
    if not test_bugs:
        for selector in cmd_args:
            if selector.startswith('bugs'):
                test_bugs = True
2615

Robert Bradshaw's avatar
Robert Bradshaw committed
2616
    selectors = [ string_selector(r) for r in cmd_args ]
2617
    verbose_excludes = selectors or options.verbosity >= 2
2618
    if not selectors:
Robert Bradshaw's avatar
Robert Bradshaw committed
2619
        selectors = [ lambda x, tags=None: True ]
2620

2621
    # Check which external modules are not present and exclude tests
2622 2623
    # which depends on them (by prefix)

2624 2625
    missing_dep_excluder = MissingDependencyExcluder(EXT_DEP_MODULES)
    version_dep_excluder = VersionDependencyExcluder(VER_DEP_MODULES)
Robert Bradshaw's avatar
cleanup  
Robert Bradshaw committed
2626
    exclude_selectors = [missing_dep_excluder, version_dep_excluder] # want to print msg at exit
2627

2628
    try:
2629 2630 2631 2632
        import IPython.core.release
        if list(IPython.core.release._ver) < [1, 0, 0]:
            raise ImportError
    except (ImportError, AttributeError, TypeError):
2633
        exclude_selectors.append(RegExSelector('IPython'))
2634

2635
    try:
2636
        raise ImportError("Jedi typer is currently broken, see GH#1845")
2637
        import jedi
2638
        if not ([0, 9] <= list(map(int, re.findall('[0-9]+', jedi.__version__ or '0')))):
2639
            raise ImportError
Stefan Behnel's avatar
Stefan Behnel committed
2640
    except (ImportError, AttributeError, TypeError):
2641 2642
        exclude_selectors.append(RegExSelector('Jedi'))

2643
    if options.exclude:
Robert Bradshaw's avatar
Robert Bradshaw committed
2644
        exclude_selectors += [ string_selector(r) for r in options.exclude ]
2645

2646 2647 2648
    if not COMPILER_HAS_INT128 or not IS_CPYTHON:
        exclude_selectors += [RegExSelector('int128')]

2649 2650 2651
    if options.shard_num > -1:
        exclude_selectors.append(ShardExcludeSelector(options.shard_num, options.shard_count))

2652
    if not test_bugs:
2653 2654 2655
        bug_files = [
            ('bugs.txt', True),
            ('pypy_bugs.txt', IS_PYPY),
2656 2657 2658
            ('pypy2_bugs.txt', IS_PYPY and IS_PY2),
            ('pypy_crash_bugs.txt', IS_PYPY),
            ('pypy_implementation_detail_bugs.txt', IS_PYPY),
2659
            ('limited_api_bugs.txt', options.limited_api),
2660 2661 2662 2663
            ('windows_bugs.txt', sys.platform == 'win32'),
            ('cygwin_bugs.txt', sys.platform == 'cygwin')
        ]

2664
        exclude_selectors += [
2665 2666 2667
            FileListExcluder(os.path.join(ROOTDIR, bugs_file_name),
                             verbose=verbose_excludes)
            for bugs_file_name, condition in bug_files if condition
2668
        ]
2669

2670 2671 2672
    global COMPILER
    if options.compiler:
        COMPILER = options.compiler
2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685

    selected_backends = [ name.strip() for name in options.backends.split(',') if name.strip() ]
    backends = []
    for backend in selected_backends:
        if backend == 'c' and not options.use_c:
            continue
        elif backend == 'cpp' and not options.use_cpp:
            continue
        elif backend not in BACKENDS:
            sys.stderr.write("Unknown backend requested: '%s' not one of [%s]\n" % (
                backend, ','.join(BACKENDS)))
            sys.exit(1)
        backends.append(backend)
2686 2687
    if options.shard_num <= 0:
        sys.stderr.write("Backends: %s\n" % ','.join(backends))
2688 2689
    languages = backends

davfsa's avatar
davfsa committed
2690 2691
    if 'CI' in os.environ and sys.platform == 'darwin' and 'cpp' in languages:
        bugs_file_name = 'macos_cpp_bugs.txt'
2692 2693 2694 2695 2696
        exclude_selectors += [
            FileListExcluder(os.path.join(ROOTDIR, bugs_file_name),
                             verbose=verbose_excludes)
        ]

2697 2698 2699 2700 2701 2702 2703
    if options.use_common_utility_dir:
        common_utility_dir = os.path.join(WORKDIR, 'utility_code')
        if not os.path.exists(common_utility_dir):
            os.makedirs(common_utility_dir)
    else:
        common_utility_dir = None

2704
    sys.stderr.write("\n")
Stefan Behnel's avatar
Stefan Behnel committed
2705

2706
    test_suite = unittest.TestSuite()
2707
    stats = Stats()
2708 2709

    if options.unittests:
2710
        collect_unittests(UNITTEST_ROOT, UNITTEST_MODULE + ".", test_suite, selectors, exclude_selectors)
2711

2712
    if options.doctests:
2713
        collect_doctests(UNITTEST_ROOT, UNITTEST_MODULE + ".", test_suite, selectors, exclude_selectors)
2714

Stefan Behnel's avatar
Stefan Behnel committed
2715
    if options.filetests and languages:
2716
        filetests = TestBuilder(ROOTDIR, WORKDIR, selectors, exclude_selectors,
gabrieldemarmiesse's avatar
gabrieldemarmiesse committed
2717 2718
                                options, options.pyregr, languages, test_bugs,
                                options.language_level, common_utility_dir,
2719 2720
                                options.pythran_dir, add_embedded_test=True, stats=stats,
                                add_cpp_locals_extra_tests=options.use_cpp_locals)
2721
        test_suite.addTest(filetests.build_suite())
2722

2723
    if options.examples and languages:
2724
        examples_workdir = os.path.join(WORKDIR, 'examples')
2725
        for subdirectory in glob.glob(os.path.join(options.examples_dir, "*/")):
2726
            filetests = TestBuilder(subdirectory, examples_workdir, selectors, exclude_selectors,
2727 2728 2729
                                    options, options.pyregr, languages, test_bugs,
                                    options.language_level, common_utility_dir,
                                    options.pythran_dir,
2730
                                    default_mode='compile', stats=stats, add_cython_import=True)
2731
            test_suite.addTest(filetests.build_suite())
2732

Stefan Behnel's avatar
Stefan Behnel committed
2733
    if options.system_pyregr and languages:
2734
        sys_pyregr_dir = os.path.join(sys.prefix, 'lib', 'python'+sys.version[:3], 'test')
2735 2736
        if not os.path.isdir(sys_pyregr_dir):
            sys_pyregr_dir = os.path.join(os.path.dirname(sys.executable), 'Lib', 'test')  # source build
2737 2738
        if os.path.isdir(sys_pyregr_dir):
            filetests = TestBuilder(ROOTDIR, WORKDIR, selectors, exclude_selectors,
gabrieldemarmiesse's avatar
gabrieldemarmiesse committed
2739
                                    options, True, languages, test_bugs,
2740
                                    sys.version_info[0], common_utility_dir, stats=stats)
2741 2742
            sys.stderr.write("Including CPython regression tests in %s\n" % sys_pyregr_dir)
            test_suite.addTest(filetests.handle_directory(sys_pyregr_dir, 'pyregr'))
2743

2744
    if options.code_style and options.shard_num <= 0:
2745 2746 2747 2748 2749 2750 2751
        try:
            import pycodestyle
        except ImportError:
            # Hack to make the exclusion visible.
            missing_dep_excluder.tests_missing_deps.append('TestCodeFormat')
        else:
            test_suite.addTest(TestCodeFormat(options.cython_dir))
2752

2753
    if xml_output_dir:
2754
        from Cython.Tests.xmlrunner import XMLTestRunner
2755
        if not os.path.exists(xml_output_dir):
2756 2757 2758 2759
            try:
                os.makedirs(xml_output_dir)
            except OSError:
                pass  # concurrency issue?
2760
        test_runner = XMLTestRunner(output=xml_output_dir,
Stefan Behnel's avatar
Stefan Behnel committed
2761
                                    verbose=options.verbosity > 0)
2762
        if options.failfast:
Stefan Behnel's avatar
Stefan Behnel committed
2763
            sys.stderr.write("--failfast not supported with XML runner\n")
2764
    else:
2765
        text_runner_options = {}
Stefan Behnel's avatar
Stefan Behnel committed
2766
        if options.failfast:
2767
            text_runner_options['failfast'] = True
2768
        test_runner = unittest.TextTestRunner(verbosity=options.verbosity, **text_runner_options)
2769

2770
    if options.pyximport_py:
2771
        from pyximport import pyximport
2772
        pyximport.install(pyimport=True, build_dir=os.path.join(WORKDIR, '_pyximport'),
2773
                          load_py_module_on_import_failure=True, inplace=True)
2774

2775 2776 2777 2778
    try:
        gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
    except AttributeError:
        pass  # not available on PyPy
2779

2780
    result = test_runner.run(test_suite)
2781

2782 2783 2784
    if common_utility_dir and options.shard_num < 0 and options.cleanup_workdir:
        shutil.rmtree(common_utility_dir)

2785 2786 2787 2788
    if missing_dep_excluder.tests_missing_deps:
        sys.stderr.write("Following tests excluded because of missing dependencies on your system:\n")
        for test in missing_dep_excluder.tests_missing_deps:
            sys.stderr.write("   %s\n" % test)
2789

2790
    if options.with_refnanny:
Dag Sverre Seljebotn's avatar
Cleanup  
Dag Sverre Seljebotn committed
2791
        import refnanny
2792
        sys.stderr.write("\n".join([repr(x) for x in refnanny.reflog]))
2793

2794 2795 2796 2797
    result_code = 0 if options.exit_ok else not result.wasSuccessful()

    if xml_output_dir:
        failure_output = ""
2798
    else:
2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814
        failure_output = "".join(collect_failure_output(result))

    return options.shard_num, stats, result_code, failure_output


def collect_failure_output(result):
    """Extract test error/failure output from a TextTestResult."""
    failure_output = []
    for flavour, errors in (("ERROR", result.errors), ("FAIL", result.failures)):
        for test, err in errors:
            failure_output.append("%s\n%s: %s\n%s\n%s\n" % (
                result.separator1,
                flavour, result.getDescription(test),
                result.separator2,
                err))
    return failure_output
2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825


if __name__ == '__main__':
    try:
        main()
    except Exception:
        traceback.print_exc()
        try:
            check_thread_termination(ignore_seen=False)
        except PendingThreadsError:
            # normal program exit won't kill the threads, do it the hard way here
2826
            flush_and_terminate(1)
2827
        sys.exit(1)