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

3 4 5
import os
import sys
import re
6
import gc
7
import locale
8
import shutil
9
import time
10 11
import unittest
import doctest
12
import operator
Robert Bradshaw's avatar
Robert Bradshaw committed
13
import subprocess
14
import tempfile
15
import traceback
Robert Bradshaw's avatar
Robert Bradshaw committed
16 17
import warnings

18 19 20 21 22 23 24 25
try:
    import platform
    IS_PYPY = platform.python_implementation() == 'PyPy'
    IS_CPYTHON = platform.python_implementation() == 'CPython'
except (ImportError, AttributeError):
    IS_CPYTHON = True
    IS_PYPY = False

26
from io import StringIO, open as io_open
27 28 29 30 31 32

try:
    import cPickle as pickle
except ImportError:
    import pickle

33 34 35 36 37
try:
    import threading
except ImportError: # No threads, no problems
    threading = None

Robert Bradshaw's avatar
Robert Bradshaw committed
38 39 40 41 42 43 44 45 46 47 48 49 50
try:
    from collections import defaultdict
except ImportError:
    class defaultdict(object):
        def __init__(self, default_factory=lambda : None):
            self._dict = {}
            self.default_factory = default_factory
        def __getitem__(self, key):
            if key not in self._dict:
                self._dict[key] = self.default_factory()
            return self._dict[key]
        def __setitem__(self, key, value):
            self._dict[key] = value
Stefan Behnel's avatar
Stefan Behnel committed
51 52
        def __contains__(self, key):
            return key in self._dict
Robert Bradshaw's avatar
Robert Bradshaw committed
53 54
        def __repr__(self):
            return repr(self._dict)
Stefan Behnel's avatar
Stefan Behnel committed
55 56
        def __nonzero__(self):
            return bool(self._dict)
Robert Bradshaw's avatar
Robert Bradshaw committed
57

Stefan Behnel's avatar
Stefan Behnel committed
58 59 60 61 62
try:
    basestring
except NameError:
    basestring = str

63
WITH_CYTHON = True
64
CY3_DIR = None
65 66

from distutils.command.build_ext import build_ext as _build_ext
67
from distutils import sysconfig
68

69

70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
def get_distutils_distro(_cache=[]):
    if _cache:
        return _cache[0]
    # late import to accomodate for setuptools override
    from distutils.dist import Distribution
    distutils_distro = Distribution()

    if sys.platform == 'win32':
        # TODO: Figure out why this hackery (see http://thread.gmane.org/gmane.comp.python.cython.devel/8280/).
        config_files = distutils_distro.find_config_files()
        try: config_files.remove('setup.cfg')
        except ValueError: pass
        distutils_distro.parse_config_files(config_files)

        cfgfiles = distutils_distro.find_config_files()
        try: cfgfiles.remove('setup.cfg')
        except ValueError: pass
        distutils_distro.parse_config_files(cfgfiles)
    _cache.append(distutils_distro)
    return distutils_distro
Robert Bradshaw's avatar
Robert Bradshaw committed
90 91


92
EXT_DEP_MODULES = {
93 94 95 96 97
    'tag:numpy':    'numpy',
    'tag:asyncio':  'asyncio',
    'tag:pstats':   'pstats',
    'tag:posix':    'posix',
    'tag:array':    'array',
98
    'tag:coverage': 'Cython.Coverage',
99 100 101
    'Coverage':     'Cython.Coverage',
    'tag:ipython':  'IPython',
    'tag:jedi':     'jedi',
102 103
}

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
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

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
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))
171
            line = next(lines)
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
            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)

192 193 194 195
def update_linetrace_extension(ext):
    ext.define_macros.append(('CYTHON_TRACE', 1))
    return ext

196
def update_numpy_extension(ext):
197
    import numpy
198 199
    from numpy.distutils.misc_util import get_info

200
    ext.include_dirs.append(numpy.get_include())
201

202 203 204 205 206
    # 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)

207
def update_openmp_extension(ext):
208
    ext.openmp = True
209 210 211 212 213 214 215 216 217 218 219 220 221
    language = ext.language

    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
222 223
    elif sys.platform == 'win32':
        return ext
224 225 226 227 228 229 230 231 232 233 234 235 236 237

    return EXCLUDE_EXT

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
    """
    if language == 'cpp':
        cc = sysconfig.get_config_var('CXX')
    else:
        cc = sysconfig.get_config_var('CC')
238 239

    if not cc:
240 241 242
        if sys.platform == 'win32':
            return '/openmp', ''
        return None
243

244 245 246
    # For some reason, cc can be e.g. 'gcc -pthread'
    cc = cc.split()[0]

247 248 249 250
    # Force english output
    env = os.environ.copy()
    env['LC_MESSAGES'] = 'C'

251 252
    matcher = re.compile(r"gcc version (\d+\.\d+)").search
    try:
253
        p = subprocess.Popen([cc, "-v"], stderr=subprocess.PIPE, env=env)
Robert Bradshaw's avatar
Robert Bradshaw committed
254 255 256 257 258 259
    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))
        return None
    _, output = p.communicate()
260

Stefan Behnel's avatar
Stefan Behnel committed
261
    output = output.decode(locale.getpreferredencoding() or 'ASCII', 'replace')
262

263 264 265 266
    gcc_version = matcher(output)
    if not gcc_version:
        return None # not gcc - FIXME: do something about other compilers

267 268 269 270
    # 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

271
    compiler_version = gcc_version.group(1)
272 273 274
    if compiler_version and compiler_version.split('.') >= ['4', '2']:
        return '-fopenmp', '-fopenmp'

275 276 277 278
try:
    locale.setlocale(locale.LC_ALL, '')
except locale.Error:
    pass
279

280 281
COMPILER = None
COMPILER_HAS_INT128 = False
282 283 284 285 286 287
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()

288 289
EXT_EXTRAS = {
    'tag:numpy' : update_numpy_extension,
290
    'tag:openmp': update_openmp_extension,
291
    'tag:trace' : update_linetrace_extension,
292
}
293

294 295 296 297 298

def _is_py3_before_32(excluded, version):
    return version[0] >= 3 and version < (3,2)


Robert Bradshaw's avatar
Robert Bradshaw committed
299
# TODO: use tags
300
VER_DEP_MODULES = {
301
    # tests are excluded if 'CurrentPythonVersion OP VersionTuple', i.e.
Stefan Behnel's avatar
Stefan Behnel committed
302
    # (2,4) : (operator.lt, ...) excludes ... when PyVer < 2.4.x
303
    (2,7) : (operator.lt, lambda x: x in ['run.withstat_py27', # multi context with statement
Stefan Behnel's avatar
Stefan Behnel committed
304
                                          'run.yield_inside_lambda',
305
                                          'run.test_dictviews',
306
                                          'run.pyclass_special_methods',
307
                                          'run.set_literals',
Stefan Behnel's avatar
Stefan Behnel committed
308
                                          ]),
309 310 311 312
    # 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...
313 314 315
    (2,999): (operator.lt, lambda x: x in ['run.special_methods_T561_py3',
                                           'run.test_raisefrom',
                                           ]),
316
    (3,): (operator.ge, lambda x: x in ['run.non_future_division',
Stefan Behnel's avatar
Stefan Behnel committed
317
                                        'compile.extsetslice',
318
                                        'compile.extdelslice',
319 320 321 322
                                        'run.special_methods_T561_py2'
                                        ]),
    (3,1): (_is_py3_before_32, lambda x: x in ['run.pyclass_special_methods',
                                               ]),
323
    (3,3) : (operator.lt, lambda x: x in ['build.package_compilation',
324
                                          'run.yield_from_py33',
325
                                          ]),
326 327
    (3,4): (operator.lt, lambda x: x in ['run.py34_signature',
                                         ]),
328 329
    (3,5): (operator.lt, lambda x: x in ['run.py35_pep492_interop',
                                         ]),
330 331
}

332
INCLUDE_DIRS = [ d for d in os.getenv('INCLUDE', '').split(os.pathsep) if d ]
333
CFLAGS = os.getenv('CFLAGS', '').split()
334
CCACHE = os.getenv('CYTHON_RUNTESTS_CCACHE', '').split()
335
TEST_SUPPORT_DIR = 'testsupport'
336

337
BACKENDS = ['c', 'cpp']
338

339 340 341
UTF8_BOM_BYTES = r'\xef\xbb\xbf'.encode('ISO-8859-1').decode('unicode_escape')


342 343 344 345 346 347 348 349 350 351
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

352

Robert Bradshaw's avatar
Robert Bradshaw committed
353
@memoize
Robert Bradshaw's avatar
Robert Bradshaw committed
354 355
def parse_tags(filepath):
    tags = defaultdict(list)
356
    parse_tag = re.compile(r'#\s*(\w+)\s*:(.*)$').match
357
    f = io_open(filepath, encoding='ISO-8859-1', errors='ignore')
358 359
    try:
        for line in f:
360 361
            # ignore BOM-like bytes and whitespace
            line = line.lstrip(UTF8_BOM_BYTES).strip()
362
            if not line:
363 364 365 366
                if tags:
                    break  # assume all tags are in one block
                else:
                    continue
367 368
            if line[0] != '#':
                break
369 370 371 372 373 374 375 376
            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)
377
                if tag not in ('mode', 'tag', 'ticket', 'cython', 'distutils', 'preparse'):
378 379 380 381 382
                    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
383 384
    finally:
        f.close()
Robert Bradshaw's avatar
Robert Bradshaw committed
385 386
    return tags

387

Robert Bradshaw's avatar
Robert Bradshaw committed
388 389
list_unchanging_dir = memoize(lambda x: os.listdir(x))

Stefan Behnel's avatar
Stefan Behnel committed
390

391 392
@memoize
def _list_pyregr_data_files(test_directory):
393 394 395 396
    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)]
397 398


399 400 401 402 403 404 405 406 407 408 409 410 411 412
def import_ext(module_name, file_path=None):
    if file_path:
        import imp
        return imp.load_dynamic(module_name, file_path)
    else:
        try:
            from importlib import invalidate_caches
        except ImportError:
            pass
        else:
            invalidate_caches()
        return __import__(module_name, globals(), locals(), ['*'])


413 414
class build_ext(_build_ext):
    def build_extension(self, ext):
415 416 417 418 419 420
        try:
            try: # Py2.7+ & Py3.2+
                compiler_obj = self.compiler_obj
            except AttributeError:
                compiler_obj = self.compiler
            if ext.language == 'c++':
421
                compiler_obj.compiler_so.remove('-Wstrict-prototypes')
422 423
            if CCACHE:
                compiler_obj.compiler_so = CCACHE + compiler_obj.compiler_so
424 425
            if getattr(ext, 'openmp', None) and compiler_obj.compiler_type == 'msvc':
                ext.extra_compile_args.append('/openmp')
426 427
        except Exception:
            pass
428
        _build_ext.build_extension(self, ext)
429 430

class ErrorWriter(object):
431
    match_error = re.compile('(warning:)?(?:.*:)?\s*([-0-9]+)\s*:\s*([-0-9]+)\s*:\s*(.*)').match
432 433 434 435
    def __init__(self):
        self.output = []
        self.write = self.output.append

436
    def _collect(self, collect_errors, collect_warnings):
437
        s = ''.join(self.output)
438
        result = []
439 440 441
        for line in s.split('\n'):
            match = self.match_error(line)
            if match:
442 443 444
                is_warning, line, column, message = match.groups()
                if (is_warning and collect_warnings) or \
                        (not is_warning and collect_errors):
445 446
                    result.append( (int(line), int(column), message.strip()) )
        result.sort()
447
        return [ "%d:%d: %s" % values for values in result ]
448 449 450 451 452 453 454 455 456

    def geterrors(self):
        return self._collect(True, False)

    def getwarnings(self):
        return self._collect(False, True)

    def getall(self):
        return self._collect(True, True)
457

458
class TestBuilder(object):
459
    def __init__(self, rootdir, workdir, selectors, exclude_selectors, annotate,
460
                 cleanup_workdir, cleanup_sharedlibs, cleanup_failures,
461 462
                 with_pyregr, cython_only, languages, test_bugs, fork, language_level,
                 common_utility_dir):
463 464
        self.rootdir = rootdir
        self.workdir = workdir
465
        self.selectors = selectors
466
        self.exclude_selectors = exclude_selectors
467
        self.annotate = annotate
468
        self.cleanup_workdir = cleanup_workdir
469
        self.cleanup_sharedlibs = cleanup_sharedlibs
470
        self.cleanup_failures = cleanup_failures
471
        self.with_pyregr = with_pyregr
472 473
        self.cython_only = cython_only
        self.languages = languages
474
        self.test_bugs = test_bugs
475
        self.fork = fork
476
        self.language_level = language_level
477
        self.common_utility_dir = common_utility_dir
478 479 480

    def build_suite(self):
        suite = unittest.TestSuite()
481 482 483
        filenames = os.listdir(self.rootdir)
        filenames.sort()
        for filename in filenames:
484
            path = os.path.join(self.rootdir, filename)
485
            if os.path.isdir(path) and filename != TEST_SUPPORT_DIR:
486 487
                if filename == 'pyregr' and not self.with_pyregr:
                    continue
488 489
                if filename == 'broken' and not self.test_bugs:
                    continue
490
                suite.addTest(
491
                    self.handle_directory(path, filename))
492 493
        if sys.platform not in ['win32']:
            # Non-Windows makefile.
494 495
            if [1 for selector in self.selectors if selector("embedded")] \
                and not [1 for selector in self.exclude_selectors if selector("embedded")]:
496
                suite.addTest(unittest.makeSuite(EmbedTest))
497 498
        return suite

499
    def handle_directory(self, path, context):
500 501 502 503
        workdir = os.path.join(self.workdir, context)
        if not os.path.exists(workdir):
            os.makedirs(workdir)

504
        suite = unittest.TestSuite()
Robert Bradshaw's avatar
Robert Bradshaw committed
505
        filenames = list_unchanging_dir(path)
506 507
        filenames.sort()
        for filename in filenames:
508 509 510
            filepath = os.path.join(path, filename)
            module, ext = os.path.splitext(filename)
            if ext not in ('.py', '.pyx', '.srctree'):
511
                continue
512 513
            if filename.startswith('.'):
                continue # certain emacs backup files
514 515 516 517
            if context == 'pyregr':
                tags = defaultdict(list)
            else:
                tags = parse_tags(filepath)
Robert Bradshaw's avatar
Robert Bradshaw committed
518
            fqmodule = "%s.%s" % (context, module)
519
            if not [ 1 for match in self.selectors
Robert Bradshaw's avatar
Robert Bradshaw committed
520
                     if match(fqmodule, tags) ]:
521
                continue
522
            if self.exclude_selectors:
523
                if [1 for match in self.exclude_selectors
Robert Bradshaw's avatar
Robert Bradshaw committed
524
                        if match(fqmodule, tags)]:
525
                    continue
526 527 528 529 530 531 532 533

            mode = 'run' # default
            if tags['mode']:
                mode = tags['mode'][0]
            elif context == 'pyregr':
                mode = 'pyregr'

            if ext == '.srctree':
534 535
                if 'cpp' not in tags['tag'] or 'cpp' in self.languages:
                    suite.addTest(EndToEndTest(filepath, workdir, self.cleanup_workdir))
536 537 538 539 540 541
                continue

            # Choose the test suite.
            if mode == 'pyregr':
                if not filename.startswith('test_'):
                    continue
542
                test_class = CythonPyregrTestCase
543
            elif mode == 'run':
544
                if module.startswith("test_"):
545
                    test_class = CythonUnitTestCase
546
                else:
547
                    test_class = CythonRunTestCase
548
            else:
549
                test_class = CythonCompileTestCase
550

551
            for test in self.build_tests(test_class, path, workdir,
Robert Bradshaw's avatar
Robert Bradshaw committed
552
                                         module, mode == 'error', tags):
553
                suite.addTest(test)
Stefan Behnel's avatar
fix  
Stefan Behnel committed
554
            if mode == 'run' and ext == '.py' and not self.cython_only:
555 556
                # additionally test file in real Python
                suite.addTest(PureDoctestTestCase(module, os.path.join(path, filename)))
557

558 559
        return suite

Robert Bradshaw's avatar
Robert Bradshaw committed
560
    def build_tests(self, test_class, path, workdir, module, expect_errors, tags):
Robert Bradshaw's avatar
Robert Bradshaw committed
561
        if 'werror' in tags['tag']:
Vitja Makarov's avatar
Vitja Makarov committed
562 563 564 565
            warning_errors = True
        else:
            warning_errors = False

566
        if expect_errors:
Robert Bradshaw's avatar
Robert Bradshaw committed
567
            if 'cpp' in tags['tag'] and 'cpp' in self.languages:
Robert Bradshaw's avatar
Robert Bradshaw committed
568 569 570
                languages = ['cpp']
            else:
                languages = self.languages[:1]
571 572
        else:
            languages = self.languages
Robert Bradshaw's avatar
Robert Bradshaw committed
573

Robert Bradshaw's avatar
Robert Bradshaw committed
574
        if 'cpp' in tags['tag'] and 'c' in languages:
575 576
            languages = list(languages)
            languages.remove('c')
Robert Bradshaw's avatar
Robert Bradshaw committed
577 578 579
        elif 'no-cpp' in tags['tag'] and 'cpp' in self.languages:
            languages = list(languages)
            languages.remove('cpp')
580 581

        preparse_list = tags.get('preparse', ['id'])
582
        tests = [ self.build_test(test_class, path, workdir, module, tags,
583 584 585
                                  language, expect_errors, warning_errors, preparse)
                  for language in languages
                  for preparse in preparse_list ]
586 587
        return tests

588
    def build_test(self, test_class, path, workdir, module, tags,
589
                   language, expect_errors, warning_errors, preparse):
590 591 592 593
        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)
594 595
        if preparse != 'id':
            workdir += '_%s' % str(preparse)
596
        return test_class(path, workdir, module, tags,
597
                          language=language,
598
                          preparse=preparse,
599 600 601 602
                          expect_errors=expect_errors,
                          annotate=self.annotate,
                          cleanup_workdir=self.cleanup_workdir,
                          cleanup_sharedlibs=self.cleanup_sharedlibs,
603
                          cleanup_failures=self.cleanup_failures,
604
                          cython_only=self.cython_only,
605
                          fork=self.fork,
Vitja Makarov's avatar
Vitja Makarov committed
606
                          language_level=self.language_level,
607 608
                          warning_errors=warning_errors,
                          common_utility_dir=self.common_utility_dir)
609

610
class CythonCompileTestCase(unittest.TestCase):
611
    def __init__(self, test_directory, workdir, module, tags, language='c', preparse='id',
612
                 expect_errors=False, annotate=False, cleanup_workdir=True,
613
                 cleanup_sharedlibs=True, cleanup_failures=True, cython_only=False,
614 615
                 fork=True, language_level=2, warning_errors=False,
                 common_utility_dir=None):
616
        self.test_directory = test_directory
617
        self.tags = tags
618 619
        self.workdir = workdir
        self.module = module
620
        self.language = language
621 622
        self.preparse = preparse
        self.name = module if self.preparse == "id" else "%s_%s" % (module, preparse)
623
        self.expect_errors = expect_errors
624
        self.annotate = annotate
625
        self.cleanup_workdir = cleanup_workdir
626
        self.cleanup_sharedlibs = cleanup_sharedlibs
627
        self.cleanup_failures = cleanup_failures
628
        self.cython_only = cython_only
629
        self.fork = fork
630
        self.language_level = language_level
Vitja Makarov's avatar
Vitja Makarov committed
631
        self.warning_errors = warning_errors
632
        self.common_utility_dir = common_utility_dir
633 634 635
        unittest.TestCase.__init__(self)

    def shortDescription(self):
636
        return "compiling (%s) %s" % (self.language, self.name)
637

Stefan Behnel's avatar
Stefan Behnel committed
638
    def setUp(self):
Vitja Makarov's avatar
Vitja Makarov committed
639
        from Cython.Compiler import Options
640 641 642 643 644
        self._saved_options = [
            (name, getattr(Options, name))
            for name in ('warning_errors', 'clear_to_none', 'error_on_unknown_names', 'error_on_uninitialized')
        ]
        self._saved_default_directives = list(Options.directive_defaults.items())
Vitja Makarov's avatar
Vitja Makarov committed
645
        Options.warning_errors = self.warning_errors
646 647
        if sys.version_info >= (3, 4):
            Options.directive_defaults['autotestdict'] = False
Vitja Makarov's avatar
Vitja Makarov committed
648

649 650
        if not os.path.exists(self.workdir):
            os.makedirs(self.workdir)
Stefan Behnel's avatar
Stefan Behnel committed
651 652 653
        if self.workdir not in sys.path:
            sys.path.insert(0, self.workdir)

654
    def tearDown(self):
Vitja Makarov's avatar
Vitja Makarov committed
655
        from Cython.Compiler import Options
656 657
        for name, value in self._saved_options:
            setattr(Options, name, value)
658
        Options.directive_defaults = dict(self._saved_default_directives)
659
        unpatch_inspect_isfunction()
Vitja Makarov's avatar
Vitja Makarov committed
660

Stefan Behnel's avatar
Stefan Behnel committed
661 662 663 664 665 666 667 668
        try:
            sys.path.remove(self.workdir)
        except ValueError:
            pass
        try:
            del sys.modules[self.module]
        except KeyError:
            pass
669 670 671
        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
672
        if os.path.exists(self.workdir):
673 674 675 676 677 678
            if cleanup_c_files and cleanup_lib_files:
                shutil.rmtree(self.workdir, ignore_errors=True)
            else:
                for rmfile in os.listdir(self.workdir):
                    if not cleanup_c_files:
                        if (rmfile[-2:] in (".c", ".h") or
679 680
                                rmfile[-4:] == ".cpp" or
                                rmfile.endswith(".html") and rmfile.startswith(self.module)):
681 682
                            continue
                    if not cleanup_lib_files and (rmfile.endswith(".so") or rmfile.endswith(".dll")):
683
                        continue
684 685 686 687 688 689 690 691
                    try:
                        rmfile = os.path.join(self.workdir, rmfile)
                        if os.path.isdir(rmfile):
                            shutil.rmtree(rmfile, ignore_errors=True)
                        else:
                            os.remove(rmfile)
                    except IOError:
                        pass
692

693
    def runTest(self):
694
        self.success = False
695
        self.runCompileTest()
696
        self.success = True
697 698

    def runCompileTest(self):
699 700 701
        return self.compile(
            self.test_directory, self.module, self.workdir,
            self.test_directory, self.expect_errors, self.annotate)
702

703 704 705 706 707
    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
708 709 710
    def build_target_filename(self, module_name):
        target = '%s.%s' % (module_name, self.language)
        return target
711

Robert Bradshaw's avatar
Robert Bradshaw committed
712
    def related_files(self, test_directory, module_name):
713
        is_related = re.compile('%s_.*[.].*' % module_name).match
Robert Bradshaw's avatar
Robert Bradshaw committed
714
        return [filename for filename in list_unchanging_dir(test_directory)
715
                if is_related(filename)]
Robert Bradshaw's avatar
Robert Bradshaw committed
716 717

    def copy_files(self, test_directory, target_directory, file_list):
718 719 720
        if self.preparse and self.preparse != 'id':
            preparse_func = globals()[self.preparse]
            def copy(src, dest):
721
                open(dest, 'w').write(preparse_func(open(src).read()))
722 723 724 725 726 727
        else:
            # use symlink on Unix, copy on Windows
            try:
                copy = os.symlink
            except AttributeError:
                copy = shutil.copy
728 729

        join = os.path.join
Robert Bradshaw's avatar
Robert Bradshaw committed
730
        for filename in file_list:
731
            file_path = join(test_directory, filename)
732
            if os.path.exists(file_path):
733
                copy(file_path, join(target_directory, filename))
734

Robert Bradshaw's avatar
Robert Bradshaw committed
735 736 737
    def source_files(self, workdir, module_name, file_list):
        return ([self.build_target_filename(module_name)] +
            [filename for filename in file_list
738
             if not os.path.isfile(os.path.join(workdir, filename))])
739 740

    def split_source_and_output(self, test_directory, module, workdir):
741
        source_file = self.find_module_source_file(os.path.join(test_directory, module) + '.pyx')
742
        source_and_output = io_open(source_file, 'rU', encoding='ISO-8859-1')
743
        try:
744 745
            out = io_open(os.path.join(workdir, module + os.path.splitext(source_file)[1]),
                              'w', encoding='ISO-8859-1')
746 747 748 749 750 751 752 753
            for line in source_and_output:
                if line.startswith("_ERRORS"):
                    out.close()
                    out = ErrorWriter()
                else:
                    out.write(line)
        finally:
            source_and_output.close()
754

755 756 757
        try:
            geterrors = out.geterrors
        except AttributeError:
758
            out.close()
759
            return []
760 761 762
        else:
            return geterrors()

763 764
    def run_cython(self, test_directory, module, targetdir, incdir, annotate,
                   extra_compile_options=None):
765
        include_dirs = INCLUDE_DIRS + [os.path.join(test_directory, '..', TEST_SUPPORT_DIR)]
766 767
        if incdir:
            include_dirs.append(incdir)
768
        source = self.find_module_source_file(
769
            os.path.join(test_directory, module + '.pyx'))
770 771 772 773 774 775
        if self.preparse == 'id':
            source = self.find_module_source_file(
                os.path.join(test_directory, module + '.pyx'))
        else:
            self.copy_files(test_directory, targetdir, [module + '.pyx'])
            source = os.path.join(targetdir, module + '.pyx')
Stefan Behnel's avatar
Stefan Behnel committed
776
        target = os.path.join(targetdir, self.build_target_filename(module))
777

778 779
        if extra_compile_options is None:
            extra_compile_options = {}
780

781 782 783 784
        if 'allow_unknown_names' in self.tags['tag']:
            from Cython.Compiler import Options
            Options.error_on_unknown_names = False

785 786 787 788 789 790
        try:
            CompilationOptions
        except NameError:
            from Cython.Compiler.Main import CompilationOptions
            from Cython.Compiler.Main import compile as cython_compile
            from Cython.Compiler.Main import default_options
791
        common_utility_include_dir = self.common_utility_dir
792

793
        options = CompilationOptions(
794
            default_options,
795 796
            include_path = include_dirs,
            output_file = target,
797
            annotate = annotate,
798 799
            use_listing_file = False,
            cplus = self.language == 'cpp',
800
            language_level = self.language_level,
801
            generate_pxi = False,
802
            evaluate_tree_assertions = True,
803
            common_utility_include_dir = common_utility_include_dir,
804
            **extra_compile_options
805
            )
806 807 808
        cython_compile(source, options=options,
                       full_module_name=module)

809
    def run_distutils(self, test_directory, module, workdir, incdir,
810
                      extra_extension_args=None):
811 812 813
        cwd = os.getcwd()
        os.chdir(workdir)
        try:
814
            build_extension = build_ext(get_distutils_distro())
815 816 817 818
            build_extension.include_dirs = INCLUDE_DIRS[:]
            if incdir:
                build_extension.include_dirs.append(incdir)
            build_extension.finalize_options()
819 820
            if COMPILER:
                build_extension.compiler = COMPILER
821

822
            ext_compile_flags = CFLAGS[:]
823 824 825 826
            compiler = COMPILER or sysconfig.get_config_var('CC')

            if self.language == 'c' and compiler == 'gcc':
                ext_compile_flags.extend(['-std=c89', '-pedantic'])
827 828
            if  build_extension.compiler == 'mingw32':
                ext_compile_flags.append('-Wno-format')
829 830
            if extra_extension_args is None:
                extra_extension_args = {}
831

Robert Bradshaw's avatar
Robert Bradshaw committed
832 833
            related_files = self.related_files(test_directory, module)
            self.copy_files(test_directory, workdir, related_files)
834 835

            from distutils.core import Extension
836 837
            extension = Extension(
                module,
838 839
                sources=self.source_files(workdir, module, related_files),
                extra_compile_args=ext_compile_flags,
840
                **extra_extension_args
841
                )
842 843 844 845 846

            if self.language == 'cpp':
                # Set the language now as the fixer might need it
                extension.language = 'c++'

847 848 849 850 851
            if 'distutils' in self.tags:
                from Cython.Build.Dependencies import DistutilsInfo
                pyx_path = os.path.join(self.test_directory, self.module + ".pyx")
                DistutilsInfo(open(pyx_path)).apply(extension)

Stefan Behnel's avatar
Stefan Behnel committed
852
            for matcher, fixer in list(EXT_EXTRAS.items()):
853
                if isinstance(matcher, str):
Stefan Behnel's avatar
Stefan Behnel committed
854
                    # lazy init
855 856 857
                    del EXT_EXTRAS[matcher]
                    matcher = string_selector(matcher)
                    EXT_EXTRAS[matcher] = fixer
858
                if matcher(module, self.tags):
859 860 861 862
                    newext = fixer(extension)
                    if newext is EXCLUDE_EXT:
                        return
                    extension = newext or extension
863 864
            if self.language == 'cpp':
                extension.language = 'c++'
865 866 867 868 869 870
            build_extension.extensions = [extension]
            build_extension.build_temp = workdir
            build_extension.build_lib  = workdir
            build_extension.run()
        finally:
            os.chdir(cwd)
871

872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888
        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)
889

890
    def compile(self, test_directory, module, workdir, incdir,
891
                expect_errors, annotate):
892 893
        expected_errors = errors = ()
        if expect_errors:
894
            expected_errors = self.split_source_and_output(
895 896
                test_directory, module, workdir)
            test_directory = workdir
897

898 899 900 901
        if WITH_CYTHON:
            old_stderr = sys.stderr
            try:
                sys.stderr = ErrorWriter()
902
                self.run_cython(test_directory, module, workdir, incdir, annotate)
903
                errors = sys.stderr.geterrors()
904 905
            finally:
                sys.stderr = old_stderr
906

907
        tostderr = sys.__stderr__.write
908
        if 'cerror' in self.tags['tag']:
909
            if errors:
910 911 912 913
                tostderr("\n=== Expected C compile error ===\n")
                tostderr("\n=== Got Cython errors: ===\n")
                tostderr('\n'.join(errors))
                tostderr('\n\n')
914 915
                raise RuntimeError('should have generated extension code')
        elif errors or expected_errors:
916 917 918 919 920 921 922 923 924 925
            try:
                for expected, error in zip(expected_errors, errors):
                    self.assertEquals(expected, error)
                if len(errors) < len(expected_errors):
                    expected_error = expected_errors[len(errors)]
                    self.assertEquals(expected_error, None)
                elif len(errors) > len(expected_errors):
                    unexpected_error = errors[len(expected_errors)]
                    self.assertEquals(None, unexpected_error)
            except AssertionError:
926 927 928 929 930
                tostderr("\n=== Expected errors: ===\n")
                tostderr('\n'.join(expected_errors))
                tostderr("\n\n=== Got errors: ===\n")
                tostderr('\n'.join(errors))
                tostderr('\n\n')
931
                raise
932 933
            return None

934 935
        so_path = None
        if not self.cython_only:
936
            from Cython.Utils import captured_fd, print_bytes
937 938
            show_output = True
            get_stderr = get_stdout = None
939
            try:
940 941 942
                with captured_fd(1) as get_stdout:
                    with captured_fd(2) as get_stderr:
                        so_path = self.run_distutils(test_directory, module, workdir, incdir)
943
            except Exception:
944
                if 'cerror' in self.tags['tag'] and get_stderr and get_stderr():
945
                    show_output = False  # expected C compiler failure
946 947 948
                else:
                    raise
            else:
949
                if 'cerror' in self.tags['tag']:
950
                    raise RuntimeError('should have failed C compile')
951 952 953 954
            finally:
                if show_output:
                    stdout = get_stdout and get_stdout().strip()
                    if stdout:
955
                        tostderr("\n=== C/C++ compiler output: ===\n")
956
                        print_bytes(stdout, end=None, file=sys.__stderr__)
957 958
                    stderr = get_stderr and get_stderr().strip()
                    if stderr:
959
                        tostderr("\n=== C/C++ compiler error output: ===\n")
960
                        print_bytes(stderr, end=None, file=sys.__stderr__)
961
                    if stdout or stderr:
962
                        tostderr("\n==============================\n")
963
        return so_path
964

965

966
class CythonRunTestCase(CythonCompileTestCase):
967 968 969 970 971
    def setUp(self):
        CythonCompileTestCase.setUp(self)
        from Cython.Compiler import Options
        Options.clear_to_none = False

972
    def shortDescription(self):
Stefan Behnel's avatar
Stefan Behnel committed
973 974 975
        if self.cython_only:
            return CythonCompileTestCase.shortDescription(self)
        else:
976
            return "compiling (%s) and running %s" % (self.language, self.name)
977 978

    def run(self, result=None):
979 980
        if result is None:
            result = self.defaultTestResult()
Stefan Behnel's avatar
Stefan Behnel committed
981
        result.startTest(self)
982
        try:
Stefan Behnel's avatar
Stefan Behnel committed
983
            self.setUp()
984
            try:
985
                self.success = False
986
                ext_so_path = self.runCompileTest()
987
                failures, errors = len(result.failures), len(result.errors)
988
                if not self.cython_only and ext_so_path is not None:
989
                    self.run_tests(result, ext_so_path)
990 991 992
                if failures == len(result.failures) and errors == len(result.errors):
                    # No new errors...
                    self.success = True
993 994
            finally:
                check_thread_termination()
995 996 997
        except Exception:
            result.addError(self, sys.exc_info())
            result.stopTest(self)
998 999 1000 1001
        try:
            self.tearDown()
        except Exception:
            pass
1002

1003
    def run_tests(self, result, ext_so_path):
Stefan Behnel's avatar
Stefan Behnel committed
1004
        self.run_doctests(self.module, result, ext_so_path)
1005

Stefan Behnel's avatar
Stefan Behnel committed
1006
    def run_doctests(self, module_or_name, result, ext_so_path):
1007
        def run_test(result):
Stefan Behnel's avatar
Stefan Behnel committed
1008 1009 1010 1011
            if isinstance(module_or_name, basestring):
                module = import_ext(module_or_name, ext_so_path)
            else:
                module = module_or_name
1012
            tests = doctest.DocTestSuite(module)
1013 1014 1015 1016
            tests.run(result)
        run_forked_test(result, run_test, self.shortDescription(), self.fork)

def run_forked_test(result, run_func, test_name, fork=True):
Stefan Behnel's avatar
Stefan Behnel committed
1017 1018
    if not fork or sys.version_info[0] >= 3 or not hasattr(os, 'fork'):
        run_func(result)
1019 1020
        sys.stdout.flush()
        sys.stderr.flush()
1021 1022 1023 1024 1025 1026 1027 1028 1029
        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
1030
        output = None
1031
        try:
1032
            try:
1033
                tests = partial_result = None
1034
                try:
1035 1036
                    partial_result = PartialTestResult(result)
                    run_func(partial_result)
1037 1038
                    sys.stdout.flush()
                    sys.stderr.flush()
1039 1040 1041
                    gc.collect()
                except Exception:
                    result_code = 1
1042 1043 1044 1045 1046 1047 1048 1049
                    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())
1050 1051 1052 1053
                output = open(result_file, 'wb')
                pickle.dump(partial_result.data(), output)
            except:
                traceback.print_exc()
1054
        finally:
1055
            try: sys.stderr.flush()
1056
            except: pass
1057 1058 1059 1060 1061 1062 1063
            try: sys.stdout.flush()
            except: pass
            try:
                if output is not None:
                    output.close()
            except:
                pass
1064 1065 1066 1067
            os._exit(result_code)

    try:
        cid, result_code = os.waitpid(child_id, 0)
1068
        module_name = test_name.split()[-1]
1069 1070 1071 1072 1073 1074
        # os.waitpid returns the child's result code in the
        # upper byte of result_code, and the signal it was
        # killed by in the lower byte
        if result_code & 255:
            raise Exception("Tests in module '%s' were unexpectedly killed by signal %d"%
                            (module_name, result_code & 255))
1075
        result_code >>= 8
1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087
        if result_code in (0,1):
            input = open(result_file, 'rb')
            try:
                PartialTestResult.join_results(result, pickle.load(input))
            finally:
                input.close()
        if result_code:
            raise Exception("Tests in module '%s' exited with status %d" %
                            (module_name, result_code))
    finally:
        try: os.unlink(result_file)
        except: pass
1088

1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113
class PureDoctestTestCase(unittest.TestCase):
    def __init__(self, module_name, module_path):
        self.module_name = module_name
        self.module_path = module_path
        unittest.TestCase.__init__(self, 'run')

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

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

            import imp
            m = imp.load_source(loaded_module_name, self.module_path)
            try:
                doctest.DocTestSuite(m).run(result)
            finally:
                del m
                if loaded_module_name in sys.modules:
                    del sys.modules[loaded_module_name]
1114
                check_thread_termination()
1115 1116 1117 1118 1119 1120 1121
        except Exception:
            result.addError(self, sys.exc_info())
            result.stopTest(self)
        try:
            self.tearDown()
        except Exception:
            pass
1122

1123 1124 1125 1126
is_private_field = re.compile('^_[^_]').match

class _FakeClass(object):
    def __init__(self, **kwargs):
1127
        self._shortDescription = kwargs.get('module_name')
1128
        self.__dict__.update(kwargs)
1129 1130
    def shortDescription(self):
        return self._shortDescription
1131

1132 1133 1134 1135 1136 1137
try: # Py2.7+ and Py3.2+
    from unittest.runner import _TextTestResult
except ImportError:
    from unittest import _TextTestResult

class PartialTestResult(_TextTestResult):
1138
    def __init__(self, base_result):
1139
        _TextTestResult.__init__(
1140 1141 1142
            self, self._StringIO(), True,
            base_result.dots + base_result.showAll*2)

1143 1144 1145 1146 1147 1148
    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
1149
                elif attr_name != '_shortDescription':
1150 1151
                    setattr(test_case, attr_name, None)

1152
    def data(self):
1153 1154
        self.strip_error_results(self.failures)
        self.strip_error_results(self.errors)
1155 1156 1157 1158 1159 1160 1161
        return (self.failures, self.errors, self.testsRun,
                self.stream.getvalue())

    def join_results(result, data):
        """Static method for merging the result back into the main
        result object.
        """
Craig Citro's avatar
Craig Citro committed
1162
        failures, errors, tests_run, output = data
1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175
        if output:
            result.stream.write(output)
        result.errors.extend(errors)
        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)


1176
class CythonUnitTestCase(CythonRunTestCase):
1177
    def shortDescription(self):
1178
        return "compiling (%s) tests in %s" % (self.language, self.name)
1179

1180 1181 1182
    def run_tests(self, result, ext_so_path):
        module = import_ext(self.module, ext_so_path)
        unittest.defaultTestLoader.loadTestsFromModule(module).run(result)
1183 1184 1185


class CythonPyregrTestCase(CythonRunTestCase):
1186 1187 1188 1189
    def setUp(self):
        CythonRunTestCase.setUp(self)
        from Cython.Compiler import Options
        Options.error_on_unknown_names = False
1190
        Options.error_on_uninitialized = False
1191 1192 1193
        Options.directive_defaults.update(dict(
            binding=True, always_allow_keywords=True,
            set_initial_path="SOURCEFILE"))
1194
        patch_inspect_isfunction()
1195

1196 1197 1198
    def related_files(self, test_directory, module_name):
        return _list_pyregr_data_files(test_directory)

1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215
    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))
        suite.run(result)

    def _run_doctest(self, result, module):
Stefan Behnel's avatar
Stefan Behnel committed
1216
        self.run_doctests(module, result, None)
1217

1218
    def run_tests(self, result, ext_so_path):
1219
        try:
1220
            from test import support
Vitja Makarov's avatar
Vitja Makarov committed
1221 1222
        except ImportError: # Python2.x
            from test import test_support as support
1223

1224 1225 1226 1227 1228
        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)
1229

1230
            backup = (support.run_unittest, support.run_doctest)
1231 1232
            support.run_unittest = run_unittest
            support.run_doctest = run_doctest
1233

1234
            try:
1235 1236
                try:
                    sys.stdout.flush() # helps in case of crashes
1237
                    module = import_ext(self.module, ext_so_path)
1238 1239
                    sys.stdout.flush() # helps in case of crashes
                    if hasattr(module, 'test_main'):
1240 1241 1242 1243 1244 1245 1246 1247 1248
                        # 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()
1249 1250 1251 1252 1253
                        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
1254 1255

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

Stefan Behnel's avatar
Stefan Behnel committed
1257

1258
include_debugger = IS_CPYTHON and sys.version_info[:2] > (2, 5)
1259

Stefan Behnel's avatar
Stefan Behnel committed
1260

1261
def collect_unittests(path, module_prefix, suite, selectors, exclude_selectors):
1262 1263 1264 1265 1266 1267 1268
    def file_matches(filename):
        return filename.startswith("Test") and filename.endswith(".py")

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

    loader = unittest.TestLoader()
1269

1270 1271 1272
    if include_debugger:
        skipped_dirs = []
    else:
1273
        skipped_dirs = ['Cython' + os.path.sep + 'Debugger' + os.path.sep]
1274

1275
    for dirpath, dirnames, filenames in os.walk(path):
1276 1277 1278 1279 1280 1281 1282 1283 1284
        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
1285 1286 1287 1288 1289
        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")]
1290
                    modulename = module_prefix + filepath[len(path)+1:].replace(os.path.sep, '.')
Stefan Behnel's avatar
Stefan Behnel committed
1291
                    if not any(1 for match in selectors if match(modulename)):
1292
                        continue
Stefan Behnel's avatar
Stefan Behnel committed
1293
                    if any(1 for match in exclude_selectors if match(modulename)):
1294
                        continue
1295 1296 1297
                    module = __import__(modulename)
                    for x in modulename.split('.')[1:]:
                        module = getattr(module, x)
Robert Bradshaw's avatar
Robert Bradshaw committed
1298
                    suite.addTests([loader.loadTestsFromModule(module)])
1299

1300

1301
def collect_doctests(path, module_prefix, suite, selectors, exclude_selectors):
1302
    def package_matches(dirname):
1303 1304
        if dirname == 'Debugger' and not include_debugger:
            return False
1305
        return dirname not in ("Mac", "Distutils", "Plex", "Tempita")
1306
    def file_matches(filename):
Mark Florisson's avatar
Mark Florisson committed
1307
        filename, ext = os.path.splitext(filename)
1308
        blacklist = ['libcython', 'libpython', 'test_libcython_in_gdb',
1309
                     'TestLibCython']
Mark Florisson's avatar
Mark Florisson committed
1310 1311 1312 1313 1314
        return (ext == '.py' and not
                '~' in filename and not
                '#' in filename and not
                filename.startswith('.') and not
                filename in blacklist)
Stefan Behnel's avatar
Stefan Behnel committed
1315
    import doctest
1316
    for dirpath, dirnames, filenames in os.walk(path):
Robert Bradshaw's avatar
Robert Bradshaw committed
1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328
        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
1329 1330
                if [ 1 for match in exclude_selectors if match(modulename) ]:
                    continue
Robert Bradshaw's avatar
Robert Bradshaw committed
1331 1332 1333
                if 'in_gdb' in modulename:
                    # These should only be imported from gdb.
                    continue
Robert Bradshaw's avatar
Robert Bradshaw committed
1334 1335 1336 1337 1338 1339 1340 1341
                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
1342

1343 1344 1345 1346 1347 1348

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
1349
    cython_root = os.path.dirname(os.path.abspath(__file__))
1350

1351
    def __init__(self, treefile, workdir, cleanup_workdir=True):
1352
        self.name = os.path.splitext(os.path.basename(treefile))[0]
1353
        self.treefile = treefile
1354
        self.workdir = os.path.join(workdir, self.name)
1355
        self.cleanup_workdir = cleanup_workdir
1356 1357 1358
        cython_syspath = [self.cython_root]
        for path in sys.path:
            if path.startswith(self.cython_root) and path not in cython_syspath:
1359 1360
                # Py3 installation and refnanny build prepend their
                # fixed paths to sys.path => prefer that over the
1361 1362 1363
                # generic one (cython_root itself goes last)
                cython_syspath.append(path)
        self.cython_syspath = os.pathsep.join(cython_syspath[::-1])
1364 1365 1366
        unittest.TestCase.__init__(self)

    def shortDescription(self):
1367
        return "End-to-end %s" % self.name
1368 1369 1370

    def setUp(self):
        from Cython.TestUtils import unpack_source_tree
1371
        _, self.commands = unpack_source_tree(self.treefile, self.workdir)
1372 1373 1374 1375 1376 1377 1378
        self.old_dir = os.getcwd()
        os.chdir(self.workdir)
        if self.workdir not in sys.path:
            sys.path.insert(0, self.workdir)

    def tearDown(self):
        if self.cleanup_workdir:
1379 1380 1381 1382 1383 1384 1385
            for trial in range(5):
                try:
                    shutil.rmtree(self.workdir)
                except OSError:
                    time.sleep(0.1)
                else:
                    break
1386
        os.chdir(self.old_dir)
1387

1388 1389 1390 1391 1392 1393
    def _try_decode(self, content):
        try:
            return content.decode()
        except UnicodeDecodeError:
            return content.decode('iso-8859-1')

1394
    def runTest(self):
1395
        self.success = False
1396
        commands = (self.commands
Robert Bradshaw's avatar
Robert Bradshaw committed
1397
            .replace("CYTHON", "PYTHON %s" % os.path.join(self.cython_root, 'cython.py'))
1398
            .replace("PYTHON", sys.executable))
1399 1400
        old_path = os.environ.get('PYTHONPATH')
        os.environ['PYTHONPATH'] = self.cython_syspath + os.pathsep + (old_path or '')
1401
        try:
1402 1403
            for command in filter(None, commands.splitlines()):
                p = subprocess.Popen(command,
Robert Bradshaw's avatar
Robert Bradshaw committed
1404 1405 1406 1407 1408 1409 1410
                                     stderr=subprocess.PIPE,
                                     stdout=subprocess.PIPE,
                                     shell=True)
                out, err = p.communicate()
                res = p.returncode
                if res != 0:
                    print(command)
1411 1412
                    print(self._try_decode(out))
                    print(self._try_decode(err))
Robert Bradshaw's avatar
Robert Bradshaw committed
1413
                self.assertEqual(0, res, "non-zero exit status")
1414
        finally:
1415 1416 1417 1418
            if old_path:
                os.environ['PYTHONPATH'] = old_path
            else:
                del os.environ['PYTHONPATH']
1419
        self.success = True
1420 1421


1422 1423 1424 1425
# TODO: Support cython_freeze needed here as well.
# TODO: Windows support.

class EmbedTest(unittest.TestCase):
1426

1427
    working_dir = "Demos/embed"
1428

1429 1430 1431
    def setUp(self):
        self.old_dir = os.getcwd()
        os.chdir(self.working_dir)
1432
        os.system(
1433
            "make PYTHON='%s' clean > /dev/null" % sys.executable)
1434

1435 1436
    def tearDown(self):
        try:
1437 1438
            os.system(
                "make PYTHON='%s' clean > /dev/null" % sys.executable)
1439 1440 1441
        except:
            pass
        os.chdir(self.old_dir)
1442

1443
    def test_embed(self):
1444
        from distutils import sysconfig
1445
        libname = sysconfig.get_config_var('LIBRARY')
1446
        libdir = sysconfig.get_config_var('LIBDIR')
1447 1448 1449 1450 1451 1452 1453
        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')
1454
        cython = 'cython.py'
Stefan Behnel's avatar
Stefan Behnel committed
1455
        if sys.version_info[0] >=3 and CY3_DIR:
1456 1457
            cython = os.path.join(CY3_DIR, cython)
        cython = os.path.abspath(os.path.join('..', '..', cython))
1458
        self.assert_(os.system(
1459
            "make PYTHON='%s' CYTHON='%s' LIBDIR1='%s' test > make.output" % (sys.executable, cython, libdir)) == 0)
1460 1461 1462 1463
        try:
            os.remove('make.output')
        except OSError:
            pass
1464

1465 1466
class MissingDependencyExcluder:
    def __init__(self, deps):
1467
        # deps: { matcher func : module name }
1468
        self.exclude_matchers = []
1469
        for matcher, mod in deps.items():
1470 1471 1472
            try:
                __import__(mod)
            except ImportError:
Robert Bradshaw's avatar
Robert Bradshaw committed
1473
                self.exclude_matchers.append(string_selector(matcher))
1474
        self.tests_missing_deps = []
Robert Bradshaw's avatar
Robert Bradshaw committed
1475
    def __call__(self, testname, tags=None):
1476
        for matcher in self.exclude_matchers:
Robert Bradshaw's avatar
Robert Bradshaw committed
1477
            if matcher(testname, tags):
1478 1479 1480 1481
                self.tests_missing_deps.append(testname)
                return True
        return False

1482 1483 1484 1485 1486
class VersionDependencyExcluder:
    def __init__(self, deps):
        # deps: { version : matcher func }
        from sys import version_info
        self.exclude_matchers = []
1487 1488
        for ver, (compare, matcher) in deps.items():
            if compare(version_info, ver):
1489 1490
                self.exclude_matchers.append(matcher)
        self.tests_missing_deps = []
Robert Bradshaw's avatar
Robert Bradshaw committed
1491
    def __call__(self, testname, tags=None):
1492 1493 1494 1495 1496 1497
        for matcher in self.exclude_matchers:
            if matcher(testname):
                self.tests_missing_deps.append(testname)
                return True
        return False

1498

1499 1500
class FileListExcluder:

1501 1502
    def __init__(self, list_file, verbose=False):
        self.verbose = verbose
1503
        self.excludes = {}
1504 1505 1506
        self._list_file = os.path.relpath(list_file)
        with open(list_file) as f:
            for line in f:
1507 1508 1509
                line = line.strip()
                if line and line[0] != '#':
                    self.excludes[line.split()[0]] = True
1510

Robert Bradshaw's avatar
Robert Bradshaw committed
1511
    def __call__(self, testname, tags=None):
1512 1513
        exclude = (testname in self.excludes
                   or testname.split('.')[-1] in self.excludes)
1514
        if exclude and self.verbose:
1515 1516 1517
            print("Excluding %s because it's listed in %s"
                  % (testname, self._list_file))
        return exclude
1518

1519

Robert Bradshaw's avatar
Robert Bradshaw committed
1520 1521 1522 1523 1524
class TagsSelector:

    def __init__(self, tag, value):
        self.tag = tag
        self.value = value
1525

Robert Bradshaw's avatar
Robert Bradshaw committed
1526 1527 1528 1529 1530 1531 1532
    def __call__(self, testname, tags=None):
        if tags is None:
            return False
        else:
            return self.value in tags[self.tag]

class RegExSelector:
1533

Robert Bradshaw's avatar
Robert Bradshaw committed
1534
    def __init__(self, pattern_string):
1535 1536 1537 1538 1539
        try:
            self.pattern = re.compile(pattern_string, re.I|re.U)
        except re.error:
            print('Invalid pattern: %r' % pattern_string)
            raise
Robert Bradshaw's avatar
Robert Bradshaw committed
1540 1541 1542 1543 1544 1545 1546 1547 1548 1549

    def __call__(self, testname, tags=None):
        return self.pattern.search(testname)

def string_selector(s):
    ix = s.find(':')
    if ix == -1:
        return RegExSelector(s)
    else:
        return TagsSelector(s[:ix], s[ix+1:])
1550 1551 1552

class ShardExcludeSelector:
    # This is an exclude selector so it can override the (include) selectors.
1553 1554
    # It may not provide uniform distribution (in time or count), but is a
    # determanistic partition of the tests which is important.
1555 1556 1557 1558 1559 1560
    def __init__(self, shard_num, shard_count):
        self.shard_num = shard_num
        self.shard_count = shard_count

    def __call__(self, testname, tags=None):
        return abs(hash(testname)) % self.shard_count != self.shard_num
1561

Robert Bradshaw's avatar
Robert Bradshaw committed
1562

1563 1564 1565 1566
def refactor_for_py3(distdir, cy3_dir):
    # need to convert Cython sources first
    import lib2to3.refactor
    from distutils.util import copydir_run_2to3
1567 1568
    with open('2to3-fixers.txt') as f:
        fixers = [line.strip() for line in f if line.strip()]
1569 1570 1571
    if not os.path.exists(cy3_dir):
        os.makedirs(cy3_dir)
    import distutils.log as dlog
1572
    dlog.set_threshold(dlog.INFO)
1573 1574 1575 1576 1577 1578
    copydir_run_2to3(distdir, cy3_dir, fixer_names=fixers,
                     template = '''
                     global-exclude *
                     graft Cython
                     recursive-exclude Cython *
                     recursive-include Cython *.py *.pyx *.pxd
1579
                     recursive-include Cython/Debugger/Tests *
1580
                     recursive-include Cython/Utility *
1581
                     recursive-exclude pyximport test
1582
                     include Tools/*.py
1583
                     include pyximport/*.py
1584
                     include runtests.py
1585
                     include cython.py
1586
                     include cythonize.py
1587 1588 1589
                     ''')
    sys.path.insert(0, cy3_dir)

1590

1591 1592
class PendingThreadsError(RuntimeError):
    pass
1593

1594 1595 1596
threads_seen = []

def check_thread_termination(ignore_seen=True):
1597 1598 1599 1600 1601 1602 1603 1604 1605
    if threading is None: # no threading enabled in CPython
        return
    current = threading.currentThread()
    blocking_threads = []
    for t in threading.enumerate():
        if not t.isAlive() or t == current:
            continue
        t.join(timeout=2)
        if t.isAlive():
1606 1607 1608
            if not ignore_seen:
                blocking_threads.append(t)
                continue
1609 1610 1611 1612 1613 1614
            for seen in threads_seen:
                if t is seen:
                    break
            else:
                threads_seen.append(t)
                blocking_threads.append(t)
1615 1616 1617 1618 1619
    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))
1620
    raise PendingThreadsError("left-over threads found after running test")
1621

1622 1623
def subprocess_output(cmd):
    try:
Mark Florisson's avatar
Mark Florisson committed
1624 1625
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        return p.communicate()[0].decode('UTF-8')
1626 1627 1628 1629 1630 1631 1632 1633
    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
1634
        old_dir = os.getcwd()
1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647
        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

1648 1649 1650 1651 1652 1653 1654 1655
_orig_stdout, _orig_stderr = sys.stdout, sys.stderr
def flush_and_terminate(status):
    try:
        _orig_stdout.flush()
        _orig_stderr.flush()
    finally:
        os._exit(status)

1656
def main():
1657

1658
    global DISTDIR, WITH_CYTHON
1659 1660
    DISTDIR = os.path.join(os.getcwd(), os.path.dirname(sys.argv[0]))

1661 1662 1663
    from optparse import OptionParser
    parser = OptionParser()
    parser.add_option("--no-cleanup", dest="cleanup_workdir",
Stefan Behnel's avatar
Stefan Behnel committed
1664 1665
                      action="store_false", default=True,
                      help="do not delete the generated C files (allows passing --no-cython on next run)")
1666 1667 1668
    parser.add_option("--no-cleanup-sharedlibs", dest="cleanup_sharedlibs",
                      action="store_false", default=True,
                      help="do not delete the generated shared libary files (allows manual module experimentation)")
1669 1670 1671
    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
1672 1673 1674
    parser.add_option("--no-cython", dest="with_cython",
                      action="store_false", default=True,
                      help="do not run the Cython compiler, only the C compiler")
1675 1676
    parser.add_option("--compiler", dest="compiler", default=None,
                      help="C compiler type")
1677 1678 1679
    backend_list = ','.join(BACKENDS)
    parser.add_option("--backends", dest="backends", default=backend_list,
                      help="select backends to test (default: %s)" % backend_list)
1680 1681
    parser.add_option("--no-c", dest="use_c",
                      action="store_false", default=True,
1682
                      help="do not test C compilation backend")
1683 1684
    parser.add_option("--no-cpp", dest="use_cpp",
                      action="store_false", default=True,
1685
                      help="do not test C++ compilation backend")
1686 1687 1688
    parser.add_option("--no-unit", dest="unittests",
                      action="store_false", default=True,
                      help="do not run the unit tests")
1689 1690 1691
    parser.add_option("--no-doctest", dest="doctests",
                      action="store_false", default=True,
                      help="do not run the doctests")
1692 1693 1694
    parser.add_option("--no-file", dest="filetests",
                      action="store_false", default=True,
                      help="do not run the file based tests")
1695 1696
    parser.add_option("--no-pyregr", dest="pyregr",
                      action="store_false", default=True,
1697
                      help="do not run the regression tests of CPython in tests/pyregr/")
1698
    parser.add_option("--cython-only", dest="cython_only",
1699 1700
                      action="store_true", default=False,
                      help="only compile pyx to c, do not run C compiler or run the tests")
1701
    parser.add_option("--no-refnanny", dest="with_refnanny",
1702
                      action="store_false", default=True,
1703
                      help="do not regression test reference counting")
1704 1705 1706
    parser.add_option("--no-fork", dest="fork",
                      action="store_false", default=True,
                      help="do not fork to run tests")
1707 1708 1709
    parser.add_option("--sys-pyregr", dest="system_pyregr",
                      action="store_true", default=False,
                      help="run the regression tests of the CPython installation")
1710 1711 1712
    parser.add_option("-x", "--exclude", dest="exclude",
                      action="append", metavar="PATTERN",
                      help="exclude tests matching the PATTERN")
1713
    parser.add_option("-j", "--shard_count", dest="shard_count", metavar="N",
1714 1715 1716 1717 1718
                      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")
1719
    parser.add_option("-C", "--coverage", dest="coverage",
Stefan Behnel's avatar
Stefan Behnel committed
1720 1721
                      action="store_true", default=False,
                      help="collect source coverage data for the Compiler")
1722 1723 1724
    parser.add_option("--coverage-xml", dest="coverage_xml",
                      action="store_true", default=False,
                      help="collect source coverage data for the Compiler in XML format")
1725 1726 1727
    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
1728
    parser.add_option("-A", "--annotate", dest="annotate_source",
1729
                      action="store_true", default=True,
Stefan Behnel's avatar
Stefan Behnel committed
1730
                      help="generate annotated HTML versions of the test source files")
1731 1732 1733
    parser.add_option("--no-annotate", dest="annotate_source",
                      action="store_false",
                      help="do not generate annotated HTML versions of the test source files")
1734
    parser.add_option("-v", "--verbose", dest="verbosity",
Stefan Behnel's avatar
Stefan Behnel committed
1735 1736
                      action="count", default=0,
                      help="display test progress, pass twice to print test names")
1737 1738
    parser.add_option("-T", "--ticket", dest="tickets",
                      action="append",
1739
                      help="a bug ticket number to run the respective test in 'tests/*'")
1740 1741 1742
    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)'")
1743 1744
    parser.add_option("--xml-output", dest="xml_output_dir", metavar="DIR",
                      help="write test results in XML to directory DIR")
1745 1746 1747
    parser.add_option("--exit-ok", dest="exit_ok", default=False,
                      action="store_true",
                      help="exit without error code even on test failures")
1748 1749 1750 1751
    parser.add_option("--root-dir", dest="root_dir", default=os.path.join(DISTDIR, 'tests'),
                      help="working directory")
    parser.add_option("--work-dir", dest="work_dir", default=os.path.join(os.getcwd(), 'BUILD'),
                      help="working directory")
1752 1753
    parser.add_option("--cython-dir", dest="cython_dir", default=os.getcwd(),
                      help="Cython installation directory (default: use local source version)")
1754 1755
    parser.add_option("--debug", dest="for_debugging", default=False, action="store_true",
                      help="configure for easier use with a debugger (e.g. gdb)")
1756 1757
    parser.add_option("--pyximport-py", dest="pyximport_py", default=False, action="store_true",
                      help="use pyximport to automatically compile imported .pyx and .py files")
1758 1759
    parser.add_option("--watermark", dest="watermark", default=None,
                      help="deterministic generated by string")
1760
    parser.add_option("--use_common_utility_dir", default=False, action="store_true")
1761
    parser.add_option("--use_formal_grammar", default=False, action="store_true")
1762 1763 1764

    options, cmd_args = parser.parse_args()

1765
    WORKDIR = os.path.abspath(options.work_dir)
1766

1767 1768 1769
    if options.with_cython and sys.version_info[0] >= 3:
        sys.path.insert(0, options.cython_dir)
        if sys.version_info[:2] == (3, 2):
1770 1771 1772 1773
            try:
                # try if Cython is installed in a Py3 version
                import Cython.Compiler.Main
            except Exception:
Stefan Behnel's avatar
Stefan Behnel committed
1774 1775
                # back out anything the import process loaded, then
                # 2to3 the Cython sources to make them re-importable
1776
                cy_modules = [ name for name in sys.modules
Stefan Behnel's avatar
Stefan Behnel committed
1777
                               if name == 'Cython' or name.startswith('Cython.') ]
1778 1779 1780
                for name in cy_modules:
                    del sys.modules[name]
                # hasn't been refactored yet - do it now
1781 1782
                global CY3_DIR
                CY3_DIR = cy3_dir = os.path.join(WORKDIR, 'Cy3')
Stefan Behnel's avatar
Stefan Behnel committed
1783
                refactor_for_py3(DISTDIR, cy3_dir)
1784

1785 1786 1787 1788
    if options.watermark:
        import Cython.Compiler.Version
        Cython.Compiler.Version.watermark = options.watermark

1789
    WITH_CYTHON = options.with_cython
1790

1791
    coverage = None
1792
    if options.coverage or options.coverage_xml or options.coverage_html:
1793
        if options.shard_count <= 1 and options.shard_num < 0:
1794 1795 1796
            if not WITH_CYTHON:
                options.coverage = options.coverage_xml = options.coverage_html = False
            else:
1797
                print("Enabling coverage analysis")
1798
                from coverage import coverage as _coverage
1799
                coverage = _coverage(branch=True, omit=['Test*'])
1800 1801
                coverage.erase()
                coverage.start()
1802

1803 1804 1805
    if options.xml_output_dir:
        shutil.rmtree(options.xml_output_dir, ignore_errors=True)

1806
    if WITH_CYTHON:
1807
        global CompilationOptions, pyrex_default_options, cython_compile
1808 1809 1810 1811
        from Cython.Compiler.Main import \
            CompilationOptions, \
            default_options as pyrex_default_options, \
            compile as cython_compile
1812 1813
        from Cython.Compiler import Errors
        Errors.LEVEL = 0 # show all warnings
Stefan Behnel's avatar
Stefan Behnel committed
1814
        from Cython.Compiler import Options
1815
        Options.generate_cleanup_code = 3   # complete cleanup code
Stefan Behnel's avatar
Stefan Behnel committed
1816 1817
        from Cython.Compiler import DebugFlags
        DebugFlags.debug_temp_code_comments = 1
1818
        pyrex_default_options['formal_grammar'] = options.use_formal_grammar
1819

1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832
    if options.shard_count > 1 and options.shard_num == -1:
        import multiprocessing
        pool = multiprocessing.Pool(options.shard_count)
        tasks = [(options, cmd_args, shard_num) for shard_num in range(options.shard_count)]
        errors = []
        for shard_num, return_code in pool.imap_unordered(runtests_callback, tasks):
            if return_code != 0:
                errors.append(shard_num)
                print("FAILED (%s/%s)" % (shard_num, options.shard_count))
            print("ALL DONE (%s/%s)" % (shard_num, options.shard_count))
        pool.close()
        pool.join()
        if errors:
Stefan Behnel's avatar
Stefan Behnel committed
1833
            print("Errors for shards %s" % ", ".join([str(e) for e in errors]))
1834 1835 1836 1837
            return_code = 1
        else:
            return_code = 0
    else:
1838
        _, return_code = runtests(options, cmd_args, coverage)
1839 1840 1841 1842 1843 1844 1845
    print("ALL DONE")

    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
1846 1847
    else:
        sys.exit(return_code)
1848 1849 1850 1851 1852 1853 1854


def runtests_callback(args):
    options, cmd_args, shard_num = args
    options.shard_num = shard_num
    return runtests(options, cmd_args)

1855
def runtests(options, cmd_args, coverage=None):
1856

1857 1858 1859 1860
    WITH_CYTHON = options.with_cython
    ROOTDIR = os.path.abspath(options.root_dir)
    WORKDIR = os.path.abspath(options.work_dir)

1861
    xml_output_dir = options.xml_output_dir
1862 1863
    if options.shard_num > -1:
        WORKDIR = os.path.join(WORKDIR, str(options.shard_num))
1864 1865
        if xml_output_dir:
            xml_output_dir = os.path.join(xml_output_dir, 'shard-%03d' % options.shard_num)
1866

1867
    # RUN ALL TESTS!
1868
    UNITTEST_MODULE = "Cython"
1869
    UNITTEST_ROOT = os.path.join(os.path.dirname(__file__), UNITTEST_MODULE)
1870 1871
    if WITH_CYTHON:
        if os.path.exists(WORKDIR):
1872
            for path in os.listdir(WORKDIR):
1873
                if path in ("support", "Cy3"): continue
1874
                shutil.rmtree(os.path.join(WORKDIR, path), ignore_errors=True)
1875 1876
    if not os.path.exists(WORKDIR):
        os.makedirs(WORKDIR)
1877

1878 1879 1880 1881 1882 1883 1884
    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")
1885

1886 1887 1888 1889
    if options.for_debugging:
        options.cleanup_workdir = False
        options.cleanup_sharedlibs = False
        options.fork = False
1890
        if WITH_CYTHON and include_debugger:
1891 1892 1893
            from Cython.Compiler.Main import default_options as compiler_default_options
            compiler_default_options['gdb_debug'] = True
            compiler_default_options['output_dir'] = os.getcwd()
1894

1895 1896 1897 1898 1899
    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"))
1900
        sys.path.insert(0, os.path.split(libpath)[0])
1901
        CFLAGS.append("-DCYTHON_REFNANNY=1")
1902

1903
    if xml_output_dir and options.fork:
Stefan Behnel's avatar
Stefan Behnel committed
1904 1905 1906 1907
        # doesn't currently work together
        sys.stderr.write("Disabling forked testing to support XML test output\n")
        options.fork = False

1908 1909 1910
    if WITH_CYTHON and options.language_level == 3:
        sys.stderr.write("Using Cython language level 3.\n")

1911
    test_bugs = False
Stefan Behnel's avatar
Stefan Behnel committed
1912 1913 1914
    if options.tickets:
        for ticket_number in options.tickets:
            test_bugs = True
1915
            cmd_args.append('ticket:%s' % ticket_number)
1916 1917 1918 1919
    if not test_bugs:
        for selector in cmd_args:
            if selector.startswith('bugs'):
                test_bugs = True
1920

Robert Bradshaw's avatar
Robert Bradshaw committed
1921
    selectors = [ string_selector(r) for r in cmd_args ]
1922
    verbose_excludes = selectors or options.verbosity >= 2
1923
    if not selectors:
Robert Bradshaw's avatar
Robert Bradshaw committed
1924
        selectors = [ lambda x, tags=None: True ]
1925

1926
    # Check which external modules are not present and exclude tests
1927 1928
    # which depends on them (by prefix)

1929 1930
    missing_dep_excluder = MissingDependencyExcluder(EXT_DEP_MODULES)
    version_dep_excluder = VersionDependencyExcluder(VER_DEP_MODULES)
Robert Bradshaw's avatar
Robert Bradshaw committed
1931
    exclude_selectors = [missing_dep_excluder, version_dep_excluder] # want to print msg at exit
1932

1933
    try:
1934 1935 1936 1937
        import IPython.core.release
        if list(IPython.core.release._ver) < [1, 0, 0]:
            raise ImportError
    except (ImportError, AttributeError, TypeError):
1938
        exclude_selectors.append(RegExSelector('IPython'))
1939

1940 1941
    try:
        import jedi
1942
        if not ([0, 8, 1] <= list(map(int, re.findall('[0-9]+', jedi.__version__ or '0'))) < [0, 9]):
1943
            raise ImportError
Stefan Behnel's avatar
Stefan Behnel committed
1944
    except (ImportError, AttributeError, TypeError):
1945 1946
        exclude_selectors.append(RegExSelector('Jedi'))

1947
    if options.exclude:
Robert Bradshaw's avatar
Robert Bradshaw committed
1948
        exclude_selectors += [ string_selector(r) for r in options.exclude ]
1949

1950 1951 1952
    if not COMPILER_HAS_INT128 or not IS_CPYTHON:
        exclude_selectors += [RegExSelector('int128')]

1953 1954 1955
    if options.shard_num > -1:
        exclude_selectors.append(ShardExcludeSelector(options.shard_num, options.shard_count))

1956
    if not test_bugs:
1957 1958 1959 1960
        exclude_selectors += [
            FileListExcluder(os.path.join(ROOTDIR, bugs_file_name), verbose=verbose_excludes)
            for bugs_file_name in ['bugs.txt'] + (['pypy_bugs.txt'] if IS_PYPY else [])
        ]
1961

1962 1963
    if sys.platform in ['win32', 'cygwin'] and sys.version_info < (2,6):
        exclude_selectors += [ lambda x: x == "run.specialfloat" ]
1964

1965 1966 1967
    global COMPILER
    if options.compiler:
        COMPILER = options.compiler
1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980

    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)
1981 1982
    if options.shard_num <= 0:
        sys.stderr.write("Backends: %s\n" % ','.join(backends))
1983 1984
    languages = backends

1985 1986 1987 1988 1989 1990 1991
    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

1992
    sys.stderr.write("\n")
1993

1994 1995 1996
    test_suite = unittest.TestSuite()

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

1999
    if options.doctests:
2000
        collect_doctests(UNITTEST_ROOT, UNITTEST_MODULE + ".", test_suite, selectors, exclude_selectors)
2001

2002
    if options.filetests and languages:
2003
        filetests = TestBuilder(ROOTDIR, WORKDIR, selectors, exclude_selectors,
2004
                                options.annotate_source, options.cleanup_workdir,
2005 2006
                                options.cleanup_sharedlibs, options.cleanup_failures,
                                options.pyregr,
2007
                                options.cython_only, languages, test_bugs,
2008 2009
                                options.fork, options.language_level,
                                common_utility_dir)
2010 2011
        test_suite.addTest(filetests.build_suite())

2012
    if options.system_pyregr and languages:
2013 2014 2015 2016
        sys_pyregr_dir = os.path.join(sys.prefix, 'lib', 'python'+sys.version[:3], 'test')
        if os.path.isdir(sys_pyregr_dir):
            filetests = TestBuilder(ROOTDIR, WORKDIR, selectors, exclude_selectors,
                                    options.annotate_source, options.cleanup_workdir,
2017 2018
                                    options.cleanup_sharedlibs, options.cleanup_failures,
                                    True,
2019
                                    options.cython_only, languages, test_bugs,
2020 2021
                                    options.fork, sys.version_info[0],
                                    common_utility_dir)
2022 2023
            sys.stderr.write("Including CPython regression tests in %s\n" % sys_pyregr_dir)
            test_suite.addTest(filetests.handle_directory(sys_pyregr_dir, 'pyregr'))
2024

2025
    if xml_output_dir:
2026
        from Cython.Tests.xmlrunner import XMLTestRunner
2027 2028 2029 2030
        if not os.path.exists(xml_output_dir):
            try: os.makedirs(xml_output_dir)
            except OSError: pass  # concurrency issue?
        test_runner = XMLTestRunner(output=xml_output_dir,
Stefan Behnel's avatar
Stefan Behnel committed
2031
                                    verbose=options.verbosity > 0)
2032 2033 2034
    else:
        test_runner = unittest.TextTestRunner(verbosity=options.verbosity)

2035
    if options.pyximport_py:
2036
        from pyximport import pyximport
2037
        pyximport.install(pyimport=True, build_dir=os.path.join(WORKDIR, '_pyximport'),
2038
                          load_py_module_on_import_failure=True, inplace=True)
2039

2040
    result = test_runner.run(test_suite)
2041

2042 2043 2044
    if common_utility_dir and options.shard_num < 0 and options.cleanup_workdir:
        shutil.rmtree(common_utility_dir)

2045
    if coverage is not None:
2046
        coverage.stop()
2047 2048
        ignored_modules = set(
            'Cython.Compiler.' + name
2049 2050 2051
            for name in ('Version', 'DebugFlags', 'CmdLine')) | set(
            'Cython.' + name
            for name in ('Debugging',))
2052 2053 2054 2055 2056 2057 2058 2059 2060
        ignored_packages = ['Cython.Runtime', 'Cython.Tempita']
        modules = [
            module for name, module in sys.modules.items()
            if module is not None and
            name.startswith('Cython.') and
            '.Tests' not in name and
            name not in ignored_modules and
            not any(name.startswith(package) for package in ignored_packages)
        ]
2061 2062 2063
        if options.coverage:
            coverage.report(modules, show_missing=0)
        if options.coverage_xml:
2064
            coverage.xml_report(modules, outfile="coverage-report.xml")
2065 2066
        if options.coverage_html:
            coverage.html_report(modules, directory="coverage-report-html")
2067 2068 2069 2070 2071

    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)
2072

2073
    if options.with_refnanny:
Dag Sverre Seljebotn's avatar
Dag Sverre Seljebotn committed
2074
        import refnanny
2075
        sys.stderr.write("\n".join([repr(x) for x in refnanny.reflog]))
2076

2077
    if options.exit_ok:
2078
        return options.shard_num, 0
2079
    else:
2080
        return options.shard_num, not result.wasSuccessful()
2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091


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
2092
            flush_and_terminate(1)