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

3 4
from __future__ import print_function

5
import atexit
6 7 8
import os
import sys
import re
9
import gc
10
import heapq
11
import locale
12
import shutil
13
import time
14 15
import unittest
import doctest
16
import operator
17
import subprocess
18
import tempfile
19
import traceback
20
import warnings
21
import zlib
22
import glob
23
from contextlib import contextmanager
24

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

33 34 35 36 37
from io import open as io_open
try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO  # doesn't accept 'str' in Py2
38 39 40 41 42 43

try:
    import cPickle as pickle
except ImportError:
    import pickle

44 45 46 47 48
try:
    import threading
except ImportError: # No threads, no problems
    threading = None

Robert Bradshaw's avatar
Robert Bradshaw committed
49 50 51 52 53 54 55 56 57 58 59 60 61
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
62 63
        def __contains__(self, key):
            return key in self._dict
Robert Bradshaw's avatar
Robert Bradshaw committed
64 65
        def __repr__(self):
            return repr(self._dict)
Stefan Behnel's avatar
Stefan Behnel committed
66 67
        def __nonzero__(self):
            return bool(self._dict)
Robert Bradshaw's avatar
Robert Bradshaw committed
68

69 70 71
try:
    from unittest import SkipTest
except ImportError:
72 73
    class SkipTest(Exception):  # don't raise, only provided to allow except-ing it!
        pass
74
    def skip_test(reason):
75
        sys.stderr.write("Skipping test: %s\n" % reason)
76 77 78 79
else:
    def skip_test(reason):
        raise SkipTest(reason)

Stefan Behnel's avatar
Stefan Behnel committed
80 81 82 83 84
try:
    basestring
except NameError:
    basestring = str

85
WITH_CYTHON = True
86
CY3_DIR = None
87 88

from distutils.command.build_ext import build_ext as _build_ext
89
from distutils import sysconfig
90
from distutils import ccompiler
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
_to_clean = []

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

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

109

110 111 112
def get_distutils_distro(_cache=[]):
    if _cache:
        return _cache[0]
Unknown's avatar
Unknown committed
113
    # late import to accommodate for setuptools override
114 115 116 117 118 119
    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()
120 121 122 123
        try:
            config_files.remove('setup.cfg')
        except ValueError:
            pass
124 125 126
        distutils_distro.parse_config_files(config_files)

        cfgfiles = distutils_distro.find_config_files()
127 128 129 130
        try:
            cfgfiles.remove('setup.cfg')
        except ValueError:
            pass
131 132 133
        distutils_distro.parse_config_files(cfgfiles)
    _cache.append(distutils_distro)
    return distutils_distro
134 135


136
EXT_DEP_MODULES = {
137 138
    'tag:numpy':     'numpy',
    'tag:numpy_old': 'numpy',
139
    'tag:pythran':  'pythran',
140
    'tag:setuptools':  'setuptools.sandbox',
141 142 143 144
    'tag:asyncio':  'asyncio',
    'tag:pstats':   'pstats',
    'tag:posix':    'posix',
    'tag:array':    'array',
145
    'tag:coverage': 'Cython.Coverage',
146
    'Coverage':     'Cython.Coverage',
147
    'tag:ipython':  'IPython.testing.globalipapp',
148
    'tag:jedi':     'jedi_BROKEN_AND_DISABLED',
149
    'tag:test.support': 'test.support',  # support module for CPython unit tests
150 151
}

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
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

169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
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))
219
            line = next(lines)
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
            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)

240 241 242 243 244 245 246

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


247 248 249 250 251 252
def exclude_extension_on_platform(*platforms):
    def check(ext):
        return EXCLUDE_EXT if sys.platform in platforms else ext
    return check


253 254 255 256
def update_linetrace_extension(ext):
    ext.define_macros.append(('CYTHON_TRACE', 1))
    return ext

257

258 259 260 261 262
def update_old_numpy_extension(ext):
    update_numpy_extension(ext, set_api17_macro=False)


def update_numpy_extension(ext, set_api17_macro=True):
263
    import numpy
264 265
    from numpy.distutils.misc_util import get_info

266
    ext.include_dirs.append(numpy.get_include())
267

268 269 270
    if set_api17_macro:
        ext.define_macros.append(('NPY_NO_DEPRECATED_API', 'NPY_1_7_API_VERSION'))

271 272 273 274 275
    # 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)

276

277
def update_openmp_extension(ext):
278
    ext.openmp = True
279 280
    language = ext.language

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

285 286 287 288 289 290 291 292 293 294
    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
295 296
    elif sys.platform == 'win32':
        return ext
297 298 299

    return EXCLUDE_EXT

300

301
def update_cpp11_extension(ext):
302
    """
303 304 305
        update cpp11 extensions that will run on versions of gcc >4.8
    """
    gcc_version = get_gcc_version(ext.language)
306
    if gcc_version:
307 308
        compiler_version = gcc_version.group(1)
        if float(compiler_version) > 4.8:
309
            ext.extra_compile_args.append("-std=c++11")
310
        return ext
311

Stefan Behnel's avatar
Stefan Behnel committed
312 313
    clang_version = get_clang_version(ext.language)
    if clang_version:
314
        ext.extra_compile_args.append("-std=c++11")
315 316 317
        if sys.platform == "darwin":
          ext.extra_compile_args.append("-stdlib=libc++")
          ext.extra_compile_args.append("-mmacosx-version-min=10.7")
318 319
        return ext

320
    return EXCLUDE_EXT
321

322

323
def get_cc_version(language):
324 325
    """
        finds gcc version using Popen
326 327 328 329 330
    """
    if language == 'cpp':
        cc = sysconfig.get_config_var('CXX')
    else:
        cc = sysconfig.get_config_var('CC')
331 332
    if not cc:
       cc = ccompiler.get_default_compiler()
333 334

    if not cc:
335
        return ''
336

337 338 339
    # For some reason, cc can be e.g. 'gcc -pthread'
    cc = cc.split()[0]

340 341 342
    # Force english output
    env = os.environ.copy()
    env['LC_MESSAGES'] = 'C'
343
    try:
344
        p = subprocess.Popen([cc, "-v"], stderr=subprocess.PIPE, env=env)
345 346 347 348
    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))
349
        return ''
350
    _, output = p.communicate()
351 352 353 354 355 356 357 358 359
    return output.decode(locale.getpreferredencoding() or 'ASCII', 'replace')


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


def get_clang_version(language):
360
    matcher = re.compile(r"clang(?:-|\s+version\s+)(\d+\.\d+)").search
361
    return matcher(get_cc_version(language))
362 363 364 365 366 367 368 369 370 371 372


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

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

373
    if not gcc_version:
374 375 376 377
        if sys.platform == 'win32':
            return '/openmp', ''
        else:
            return None # not gcc - FIXME: do something about other compilers
378

379 380 381 382
    # 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

383
    compiler_version = gcc_version.group(1)
384 385 386
    if compiler_version and compiler_version.split('.') >= ['4', '2']:
        return '-fopenmp', '-fopenmp'

387 388 389 390
try:
    locale.setlocale(locale.LC_ALL, '')
except locale.Error:
    pass
391

392 393
COMPILER = None
COMPILER_HAS_INT128 = False
394 395 396 397 398 399
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()

400 401
EXT_EXTRAS = {
    'tag:numpy' : update_numpy_extension,
402
    'tag:numpy_old' : update_old_numpy_extension,
403
    'tag:openmp': update_openmp_extension,
404
    'tag:cpp11': update_cpp11_extension,
405
    'tag:trace' : update_linetrace_extension,
406
    'tag:bytesformat':  exclude_extension_in_pyver((3, 3), (3, 4)),  # no %-bytes formatting
407
    'tag:no-macos':  exclude_extension_on_platform('darwin'),
408
}
409

410

411
# TODO: use tags
412
VER_DEP_MODULES = {
413
    # tests are excluded if 'CurrentPythonVersion OP VersionTuple', i.e.
Stefan Behnel's avatar
Stefan Behnel committed
414
    # (2,4) : (operator.lt, ...) excludes ... when PyVer < 2.4.x
415
    (2,7) : (operator.lt, lambda x: x in ['run.withstat_py27', # multi context with statement
Stefan Behnel's avatar
Stefan Behnel committed
416
                                          'run.yield_inside_lambda',
417
                                          'run.test_dictviews',
418
                                          'run.pyclass_special_methods',
419
                                          'run.set_literals',
420
                                          ]),
421 422 423 424
    # 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...
425 426 427
    (2,999): (operator.lt, lambda x: x in ['run.special_methods_T561_py3',
                                           'run.test_raisefrom',
                                           ]),
428
    (3,): (operator.ge, lambda x: x in ['run.non_future_division',
Stefan Behnel's avatar
Stefan Behnel committed
429
                                        'compile.extsetslice',
430
                                        'compile.extdelslice',
431 432
                                        'run.special_methods_T561_py2'
                                        ]),
433
    (3,3) : (operator.lt, lambda x: x in ['build.package_compilation',
434
                                          'run.yield_from_py33',
435
                                          'pyximport.pyximport_namespace',
436
                                          ]),
437
    (3,4): (operator.lt, lambda x: x in ['run.py34_signature',
438
                                         'run.test_unicode',  # taken from Py3.7, difficult to backport
439
                                         ]),
440 441
    (3,4,999): (operator.gt, lambda x: x in ['run.initial_file_path',
                                             ]),
442
    (3,5): (operator.lt, lambda x: x in ['run.py35_pep492_interop',
443
                                         'run.py35_asyncio_async_def',
444
                                         'run.mod__spec__',
445
                                         'run.pep526_variable_annotations',  # typing module
446
                                         'run.test_exceptions',  # copied from Py3.7+
447
                                         ]),
448 449
}

450
INCLUDE_DIRS = [ d for d in os.getenv('INCLUDE', '').split(os.pathsep) if d ]
451
CFLAGS = os.getenv('CFLAGS', '').split()
452
CCACHE = os.getenv('CYTHON_RUNTESTS_CCACHE', '').split()
453
TEST_SUPPORT_DIR = 'testsupport'
454

455
BACKENDS = ['c', 'cpp']
456

457 458 459
UTF8_BOM_BYTES = r'\xef\xbb\xbf'.encode('ISO-8859-1').decode('unicode_escape')


460 461 462 463 464 465 466 467 468 469
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

470

Robert Bradshaw's avatar
Robert Bradshaw committed
471
@memoize
Robert Bradshaw's avatar
Robert Bradshaw committed
472 473
def parse_tags(filepath):
    tags = defaultdict(list)
474
    parse_tag = re.compile(r'#\s*(\w+)\s*:(.*)$').match
475
    with io_open(filepath, encoding='ISO-8859-1', errors='ignore') as f:
476
        for line in f:
477 478
            # ignore BOM-like bytes and whitespace
            line = line.lstrip(UTF8_BOM_BYTES).strip()
479
            if not line:
480 481 482 483
                if tags:
                    break  # assume all tags are in one block
                else:
                    continue
484 485
            if line[0] != '#':
                break
486 487 488 489 490 491 492 493
            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)
494
                if tag not in ('mode', 'tag', 'ticket', 'cython', 'distutils', 'preparse'):
495 496 497 498 499
                    print("WARNING: unknown test directive '%s' found (%s)" % (tag, filepath))
                values = values.split(',')
                tags[tag].extend(filter(None, [value.strip() for value in values]))
            elif tags:
                break  # assume all tags are in one block
Robert Bradshaw's avatar
Robert Bradshaw committed
500 501
    return tags

502

503 504
list_unchanging_dir = memoize(lambda x: os.listdir(x))

Stefan Behnel's avatar
Stefan Behnel committed
505

506 507
@memoize
def _list_pyregr_data_files(test_directory):
508 509 510 511
    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)]
512 513


514 515 516 517 518 519 520 521 522 523 524 525 526 527
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(), ['*'])


528 529
class build_ext(_build_ext):
    def build_extension(self, ext):
530 531 532 533 534 535
        try:
            try: # Py2.7+ & Py3.2+
                compiler_obj = self.compiler_obj
            except AttributeError:
                compiler_obj = self.compiler
            if ext.language == 'c++':
536
                compiler_obj.compiler_so.remove('-Wstrict-prototypes')
537 538
            if CCACHE:
                compiler_obj.compiler_so = CCACHE + compiler_obj.compiler_so
539 540
            if getattr(ext, 'openmp', None) and compiler_obj.compiler_type == 'msvc':
                ext.extra_compile_args.append('/openmp')
541 542
        except Exception:
            pass
543
        _build_ext.build_extension(self, ext)
544

545

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

549 550 551 552
    def __init__(self):
        self.output = []
        self.write = self.output.append

553
    def _collect(self):
554
        s = ''.join(self.output)
555 556
        results = {'errors': [], 'warnings': []}
        for line in s.splitlines():
557 558
            match = self.match_error(line)
            if match:
559
                is_warning, line, column, message = match.groups()
560 561 562
                results['warnings' if is_warning else 'errors'].append((int(line), int(column), message.strip()))

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

    def geterrors(self):
565
        return self._collect()[0]
566 567

    def getwarnings(self):
568
        return self._collect()[1]
569 570

    def getall(self):
571 572 573 574 575
        return self._collect()

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

576

577
class Stats(object):
578 579
    def __init__(self, top_n=8):
        self.top_n = top_n
580 581
        self.test_counts = defaultdict(int)
        self.test_times = defaultdict(float)
582
        self.top_tests = defaultdict(list)
583

584
    def add_time(self, name, language, metric, t):
585 586
        self.test_counts[metric] += 1
        self.test_times[metric] += t
587
        top = self.top_tests[metric]
588 589
        push = heapq.heappushpop if len(top) >= self.top_n else heapq.heappush
        # min-heap => pop smallest/shortest until longest times remain
590
        push(top, (t, name, language))
591 592

    @contextmanager
593
    def time(self, name, language, metric):
594 595 596
        t = time.time()
        yield
        t = time.time() - t
597
        self.add_time(name, language, metric, t)
598

599 600 601 602 603 604 605 606 607 608
    def update(self, stats):
        # type: (Stats) -> None
        for metric, t in stats.test_times.items():
            self.test_times[metric] += t
            self.test_counts[metric] += stats.test_counts[metric]
            top = self.top_tests[metric]
            for entry in stats.top_tests[metric]:
                push = heapq.heappushpop if len(top) >= self.top_n else heapq.heappush
                push(top, entry)

609
    def print_stats(self, out=sys.stderr):
610 611
        if not self.test_times:
            return
612
        lines = ['Times:\n']
613
        for metric, t in sorted(self.test_times.items()):
614
            count = self.test_counts[metric]
615 616 617
            top = self.top_tests[metric]
            lines.append("%-12s: %8.2f sec  (%4d, %6.3f / run) - slowest: %s\n" % (
                metric, t, count, t / count,
618
                ', '.join("'{2}:{1}' ({0:.2f}s)".format(*item) for item in heapq.nlargest(self.top_n, top))))
619 620 621
        out.write(''.join(lines))


622
class TestBuilder(object):
623 624
    def __init__(self, rootdir, workdir, selectors, exclude_selectors, options,
                 with_pyregr, languages, test_bugs, language_level,
625
                 common_utility_dir, pythran_dir=None,
626
                 default_mode='run', stats=None,
627
                 add_embedded_test=False):
628 629
        self.rootdir = rootdir
        self.workdir = workdir
630
        self.selectors = selectors
631
        self.exclude_selectors = exclude_selectors
632 633 634 635
        self.annotate = options.annotate_source
        self.cleanup_workdir = options.cleanup_workdir
        self.cleanup_sharedlibs = options.cleanup_sharedlibs
        self.cleanup_failures = options.cleanup_failures
636
        self.with_pyregr = with_pyregr
637
        self.cython_only = options.cython_only
638
        self.languages = languages
639
        self.test_bugs = test_bugs
640
        self.fork = options.fork
641
        self.language_level = language_level
642
        self.test_determinism = options.test_determinism
643
        self.common_utility_dir = common_utility_dir
644
        self.pythran_dir = pythran_dir
645
        self.default_mode = default_mode
646
        self.stats = stats
647
        self.add_embedded_test = add_embedded_test
648 649 650

    def build_suite(self):
        suite = unittest.TestSuite()
651 652 653
        filenames = os.listdir(self.rootdir)
        filenames.sort()
        for filename in filenames:
654
            path = os.path.join(self.rootdir, filename)
655
            if os.path.isdir(path) and filename != TEST_SUPPORT_DIR:
656 657
                if filename == 'pyregr' and not self.with_pyregr:
                    continue
658 659
                if filename == 'broken' and not self.test_bugs:
                    continue
660
                suite.addTest(
661
                    self.handle_directory(path, filename))
662
        if sys.platform not in ['win32'] and self.add_embedded_test:
663
            # Non-Windows makefile.
664 665
            if [1 for selector in self.selectors if selector("embedded")] \
                and not [1 for selector in self.exclude_selectors if selector("embedded")]:
666
                suite.addTest(unittest.makeSuite(EmbedTest))
667 668
        return suite

669
    def handle_directory(self, path, context):
670 671 672 673
        workdir = os.path.join(self.workdir, context)
        if not os.path.exists(workdir):
            os.makedirs(workdir)

674
        suite = unittest.TestSuite()
675
        filenames = list_unchanging_dir(path)
676 677
        filenames.sort()
        for filename in filenames:
678 679 680
            filepath = os.path.join(path, filename)
            module, ext = os.path.splitext(filename)
            if ext not in ('.py', '.pyx', '.srctree'):
681
                continue
682 683
            if filename.startswith('.'):
                continue # certain emacs backup files
684 685 686 687
            if context == 'pyregr':
                tags = defaultdict(list)
            else:
                tags = parse_tags(filepath)
Robert Bradshaw's avatar
Robert Bradshaw committed
688
            fqmodule = "%s.%s" % (context, module)
689
            if not [ 1 for match in self.selectors
Robert Bradshaw's avatar
Robert Bradshaw committed
690
                     if match(fqmodule, tags) ]:
691
                continue
692
            if self.exclude_selectors:
693
                if [1 for match in self.exclude_selectors
Robert Bradshaw's avatar
Robert Bradshaw committed
694
                        if match(fqmodule, tags)]:
695
                    continue
696

697
            mode = self.default_mode
698 699 700 701 702 703
            if tags['mode']:
                mode = tags['mode'][0]
            elif context == 'pyregr':
                mode = 'pyregr'

            if ext == '.srctree':
704
                if 'cpp' not in tags['tag'] or 'cpp' in self.languages:
705
                    suite.addTest(EndToEndTest(filepath, workdir, self.cleanup_workdir, stats=self.stats))
706 707 708 709 710 711
                continue

            # Choose the test suite.
            if mode == 'pyregr':
                if not filename.startswith('test_'):
                    continue
712
                test_class = CythonPyregrTestCase
713
            elif mode == 'run':
714
                if module.startswith("test_"):
715
                    test_class = CythonUnitTestCase
716
                else:
717
                    test_class = CythonRunTestCase
718
            elif mode in ['compile', 'error']:
719
                test_class = CythonCompileTestCase
720 721
            else:
                raise KeyError('Invalid test mode: ' + mode)
722

723
            for test in self.build_tests(test_class, path, workdir,
Robert Bradshaw's avatar
Robert Bradshaw committed
724
                                         module, mode == 'error', tags):
725
                suite.addTest(test)
726

727
            if mode == 'run' and ext == '.py' and not self.cython_only and not filename.startswith('test_'):
728
                # additionally test file in real Python
729 730 731 732 733 734
                min_py_ver = [
                    (int(pyver.group(1)), int(pyver.group(2)))
                    for pyver in map(re.compile(r'pure([0-9]+)[.]([0-9]+)').match, tags['tag'])
                    if pyver
                ]
                if not min_py_ver or any(sys.version_info >= min_ver for min_ver in min_py_ver):
735
                    suite.addTest(PureDoctestTestCase(module, os.path.join(path, filename), tags, stats=self.stats))
736

737 738
        return suite

Robert Bradshaw's avatar
Robert Bradshaw committed
739
    def build_tests(self, test_class, path, workdir, module, expect_errors, tags):
740 741
        warning_errors = 'werror' in tags['tag']
        expect_warnings = 'warnings' in tags['tag']
Vitja Makarov's avatar
Vitja Makarov committed
742

743
        if expect_errors:
744
            if skip_c(tags) and 'cpp' in self.languages:
Robert Bradshaw's avatar
Robert Bradshaw committed
745 746 747
                languages = ['cpp']
            else:
                languages = self.languages[:1]
748 749
        else:
            languages = self.languages
Robert Bradshaw's avatar
Robert Bradshaw committed
750

751
        if skip_c(tags) and 'c' in languages:
752 753
            languages = list(languages)
            languages.remove('c')
Robert Bradshaw's avatar
Robert Bradshaw committed
754 755 756
        elif 'no-cpp' in tags['tag'] and 'cpp' in self.languages:
            languages = list(languages)
            languages.remove('cpp')
757

758 759 760
        pythran_dir = self.pythran_dir
        if 'pythran' in tags['tag'] and not pythran_dir and 'cpp' in languages:
            import pythran.config
761 762 763 764
            try:
                pythran_ext = pythran.config.make_extension(python=True)
            except TypeError:  # old pythran version syntax
                pythran_ext = pythran.config.make_extension()
765 766
            pythran_dir = pythran_ext['include_dirs'][0]

767
        preparse_list = tags.get('preparse', ['id'])
768
        tests = [ self.build_test(test_class, path, workdir, module, tags, language,
769
                                  expect_errors, expect_warnings, warning_errors, preparse,
770
                                  pythran_dir if language == "cpp" else None)
771 772
                  for language in languages
                  for preparse in preparse_list ]
773 774
        return tests

775
    def build_test(self, test_class, path, workdir, module, tags, language,
776
                   expect_errors, expect_warnings, warning_errors, preparse, pythran_dir):
777 778 779 780
        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)
781 782
        if preparse != 'id':
            workdir += '_%s' % str(preparse)
783
        return test_class(path, workdir, module, tags,
784
                          language=language,
785
                          preparse=preparse,
786
                          expect_errors=expect_errors,
787
                          expect_warnings=expect_warnings,
788 789 790
                          annotate=self.annotate,
                          cleanup_workdir=self.cleanup_workdir,
                          cleanup_sharedlibs=self.cleanup_sharedlibs,
791
                          cleanup_failures=self.cleanup_failures,
792
                          cython_only=self.cython_only,
793
                          fork=self.fork,
Vitja Makarov's avatar
Vitja Makarov committed
794
                          language_level=self.language_level,
795
                          warning_errors=warning_errors,
796
                          test_determinism=self.test_determinism,
797
                          common_utility_dir=self.common_utility_dir,
798 799
                          pythran_dir=pythran_dir,
                          stats=self.stats)
800

801

802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817
def skip_c(tags):
    if 'cpp' in tags['tag']:
        return True

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


818 819 820 821 822 823 824 825 826 827 828 829
def filter_stderr(stderr_bytes):
    """
    Filter annoying warnings from output.
    """
    if b"Command line warning D9025" in stderr_bytes:
        # MSCV: cl : Command line warning D9025 : overriding '/Ox' with '/Od'
        stderr_bytes = b'\n'.join(
            line for line in stderr_bytes.splitlines()
            if b"Command line warning D9025" not in line)
    return stderr_bytes


830
class CythonCompileTestCase(unittest.TestCase):
831
    def __init__(self, test_directory, workdir, module, tags, language='c', preparse='id',
832
                 expect_errors=False, expect_warnings=False, annotate=False, cleanup_workdir=True,
833
                 cleanup_sharedlibs=True, cleanup_failures=True, cython_only=False,
834
                 fork=True, language_level=2, warning_errors=False,
835
                 test_determinism=False,
836
                 common_utility_dir=None, pythran_dir=None, stats=None):
837
        self.test_directory = test_directory
838
        self.tags = tags
839 840
        self.workdir = workdir
        self.module = module
841
        self.language = language
842 843
        self.preparse = preparse
        self.name = module if self.preparse == "id" else "%s_%s" % (module, preparse)
844
        self.expect_errors = expect_errors
845
        self.expect_warnings = expect_warnings
846
        self.annotate = annotate
847
        self.cleanup_workdir = cleanup_workdir
848
        self.cleanup_sharedlibs = cleanup_sharedlibs
849
        self.cleanup_failures = cleanup_failures
850
        self.cython_only = cython_only
851
        self.fork = fork
852
        self.language_level = language_level
Vitja Makarov's avatar
Vitja Makarov committed
853
        self.warning_errors = warning_errors
854
        self.test_determinism = test_determinism
855
        self.common_utility_dir = common_utility_dir
856
        self.pythran_dir = pythran_dir
857
        self.stats = stats
858 859 860
        unittest.TestCase.__init__(self)

    def shortDescription(self):
861
        return "compiling (%s%s) %s" % (self.language, "/pythran" if self.pythran_dir is not None else "", self.name)
862

Stefan Behnel's avatar
Stefan Behnel committed
863
    def setUp(self):
Vitja Makarov's avatar
Vitja Makarov committed
864
        from Cython.Compiler import Options
865 866 867 868
        self._saved_options = [
            (name, getattr(Options, name))
            for name in ('warning_errors', 'clear_to_none', 'error_on_unknown_names', 'error_on_uninitialized')
        ]
869
        self._saved_default_directives = list(Options.get_directive_defaults().items())
Vitja Makarov's avatar
Vitja Makarov committed
870
        Options.warning_errors = self.warning_errors
871
        if sys.version_info >= (3, 4):
872
            Options._directive_defaults['autotestdict'] = False
Vitja Makarov's avatar
Vitja Makarov committed
873

874 875
        if not os.path.exists(self.workdir):
            os.makedirs(self.workdir)
Stefan Behnel's avatar
Stefan Behnel committed
876 877 878
        if self.workdir not in sys.path:
            sys.path.insert(0, self.workdir)

879
    def tearDown(self):
Vitja Makarov's avatar
Vitja Makarov committed
880
        from Cython.Compiler import Options
881 882
        for name, value in self._saved_options:
            setattr(Options, name, value)
883
        Options._directive_defaults = dict(self._saved_default_directives)
884
        unpatch_inspect_isfunction()
Vitja Makarov's avatar
Vitja Makarov committed
885

Stefan Behnel's avatar
Stefan Behnel committed
886 887 888 889 890 891 892 893
        try:
            sys.path.remove(self.workdir)
        except ValueError:
            pass
        try:
            del sys.modules[self.module]
        except KeyError:
            pass
894 895 896
        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
897 898
        is_cygwin = sys.platform == 'cygwin'

899
        if os.path.exists(self.workdir):
900
            if cleanup_c_files and cleanup_lib_files and not is_cygwin:
901 902 903 904 905
                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
906 907
                                rmfile[-4:] == ".cpp" or
                                rmfile.endswith(".html") and rmfile.startswith(self.module)):
908
                            continue
909 910 911 912

                    is_shared_obj = rmfile.endswith(".so") or rmfile.endswith(".dll")

                    if not cleanup_lib_files and is_shared_obj:
913
                        continue
914

915 916 917 918
                    try:
                        rmfile = os.path.join(self.workdir, rmfile)
                        if os.path.isdir(rmfile):
                            shutil.rmtree(rmfile, ignore_errors=True)
919 920 921
                        elif is_cygwin and is_shared_obj:
                            # Delete later
                            _to_clean.append(rmfile)
922 923 924 925
                        else:
                            os.remove(rmfile)
                    except IOError:
                        pass
926

927 928 929 930
                if cleanup_c_files and cleanup_lib_files and is_cygwin:
                    # Finally, remove the work dir itself
                    _to_clean.append(self.workdir)

931 932 933 934
        if cleanup_c_files and os.path.exists(self.workdir + '-again'):
            shutil.rmtree(self.workdir + '-again', ignore_errors=True)


935
    def runTest(self):
936
        self.success = False
937
        self.runCompileTest()
938
        self.success = True
939 940

    def runCompileTest(self):
941 942
        return self.compile(
            self.test_directory, self.module, self.workdir,
943
            self.test_directory, self.expect_errors, self.expect_warnings, self.annotate)
944

945 946 947 948 949
    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
950 951 952
    def build_target_filename(self, module_name):
        target = '%s.%s' % (module_name, self.language)
        return target
953

954
    def related_files(self, test_directory, module_name):
955
        is_related = re.compile('%s_.*[.].*' % module_name).match
956
        return [filename for filename in list_unchanging_dir(test_directory)
957
                if is_related(filename)]
958 959

    def copy_files(self, test_directory, target_directory, file_list):
960 961 962
        if self.preparse and self.preparse != 'id':
            preparse_func = globals()[self.preparse]
            def copy(src, dest):
963 964 965
                with open(src) as fin:
                    with open(dest, 'w') as fout:
                        fout.write(preparse_func(fin.read()))
966 967 968 969 970 971
        else:
            # use symlink on Unix, copy on Windows
            try:
                copy = os.symlink
            except AttributeError:
                copy = shutil.copy
972 973

        join = os.path.join
974
        for filename in file_list:
975
            file_path = join(test_directory, filename)
976
            if os.path.exists(file_path):
977
                copy(file_path, join(target_directory, filename))
978

979 980 981
    def source_files(self, workdir, module_name, file_list):
        return ([self.build_target_filename(module_name)] +
            [filename for filename in file_list
982
             if not os.path.isfile(os.path.join(workdir, filename))])
983 984

    def split_source_and_output(self, test_directory, module, workdir):
985
        source_file = self.find_module_source_file(os.path.join(test_directory, module) + '.pyx')
986 987
        with io_open(source_file, 'r', encoding='ISO-8859-1') as source_and_output:
            error_writer = warnings_writer = None
988
            out = io_open(os.path.join(workdir, module + os.path.splitext(source_file)[1]),
989
                          'w', encoding='ISO-8859-1')
990 991 992 993 994 995 996 997 998 999 1000 1001
            try:
                for line in source_and_output:
                    if line.startswith("_ERRORS"):
                        out.close()
                        out = error_writer = ErrorWriter()
                    elif line.startswith("_WARNINGS"):
                        out.close()
                        out = warnings_writer = ErrorWriter()
                    else:
                        out.write(line)
            finally:
                out.close()
1002

1003 1004
        return (error_writer.geterrors() if error_writer else [],
                warnings_writer.geterrors() if warnings_writer else [])
1005

1006 1007
    def run_cython(self, test_directory, module, targetdir, incdir, annotate,
                   extra_compile_options=None):
1008
        include_dirs = INCLUDE_DIRS + [os.path.join(test_directory, '..', TEST_SUPPORT_DIR)]
1009 1010
        if incdir:
            include_dirs.append(incdir)
1011

1012 1013 1014 1015 1016 1017
        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
1018
        target = os.path.join(targetdir, self.build_target_filename(module))
1019

1020 1021
        if extra_compile_options is None:
            extra_compile_options = {}
1022

1023 1024 1025 1026
        if 'allow_unknown_names' in self.tags['tag']:
            from Cython.Compiler import Options
            Options.error_on_unknown_names = False

1027 1028 1029 1030 1031 1032
        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
1033
        common_utility_include_dir = self.common_utility_dir
1034

1035
        options = CompilationOptions(
1036
            default_options,
1037 1038
            include_path = include_dirs,
            output_file = target,
1039
            annotate = annotate,
1040 1041
            use_listing_file = False,
            cplus = self.language == 'cpp',
1042
            np_pythran = self.pythran_dir is not None,
1043
            language_level = self.language_level,
1044
            generate_pxi = False,
1045
            evaluate_tree_assertions = True,
1046
            common_utility_include_dir = common_utility_include_dir,
1047
            **extra_compile_options
1048
            )
1049 1050 1051
        cython_compile(source, options=options,
                       full_module_name=module)

1052
    def run_distutils(self, test_directory, module, workdir, incdir,
1053
                      extra_extension_args=None):
1054 1055 1056
        cwd = os.getcwd()
        os.chdir(workdir)
        try:
1057
            build_extension = build_ext(get_distutils_distro())
1058 1059 1060 1061
            build_extension.include_dirs = INCLUDE_DIRS[:]
            if incdir:
                build_extension.include_dirs.append(incdir)
            build_extension.finalize_options()
1062 1063
            if COMPILER:
                build_extension.compiler = COMPILER
1064

1065
            ext_compile_flags = CFLAGS[:]
1066

1067 1068
            if  build_extension.compiler == 'mingw32':
                ext_compile_flags.append('-Wno-format')
1069 1070
            if extra_extension_args is None:
                extra_extension_args = {}
1071

1072 1073
            related_files = self.related_files(test_directory, module)
            self.copy_files(test_directory, workdir, related_files)
1074 1075

            from distutils.core import Extension
1076 1077
            extension = Extension(
                module,
1078 1079
                sources=self.source_files(workdir, module, related_files),
                extra_compile_args=ext_compile_flags,
1080
                **extra_extension_args
1081
                )
1082 1083 1084 1085 1086

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

1087 1088
            if 'distutils' in self.tags:
                from Cython.Build.Dependencies import DistutilsInfo
1089
                from Cython.Utils import open_source_file
1090
                pyx_path = os.path.join(self.test_directory, self.module + ".pyx")
1091 1092
                with open_source_file(pyx_path) as f:
                    DistutilsInfo(f).apply(extension)
1093

1094 1095 1096 1097
            if self.pythran_dir:
                from Cython.Build.Dependencies import update_pythran_extension
                update_pythran_extension(extension)

Stefan Behnel's avatar
Stefan Behnel committed
1098
            for matcher, fixer in list(EXT_EXTRAS.items()):
1099
                if isinstance(matcher, str):
Stefan Behnel's avatar
Stefan Behnel committed
1100
                    # lazy init
1101 1102 1103
                    del EXT_EXTRAS[matcher]
                    matcher = string_selector(matcher)
                    EXT_EXTRAS[matcher] = fixer
1104
                if matcher(module, self.tags):
1105 1106
                    newext = fixer(extension)
                    if newext is EXCLUDE_EXT:
1107
                        return skip_test("Test '%s' excluded due to tags '%s'" % (
1108
                            self.name, ', '.join(self.tags.get('tag', ''))))
1109
                    extension = newext or extension
1110 1111
            if self.language == 'cpp':
                extension.language = 'c++'
1112 1113 1114 1115 1116 1117
            build_extension.extensions = [extension]
            build_extension.build_temp = workdir
            build_extension.build_lib  = workdir
            build_extension.run()
        finally:
            os.chdir(cwd)
1118

1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135
        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)