iptest.py
454 lines
| 16.4 KiB
| text/x-python
|
PythonLexer
Fernando Perez
|
r1574 | # -*- coding: utf-8 -*- | ||
"""IPython Test Suite Runner. | ||||
Fernando Perez
|
r1851 | |||
Brian Granger
|
r1972 | This module provides a main entry point to a user script to test IPython | ||
itself from the command line. There are two ways of running this script: | ||||
1. With the syntax `iptest all`. This runs our entire test suite by | ||||
Brian Granger
|
r3486 | calling this script (with different arguments) recursively. This | ||
Brian Granger
|
r1972 | causes modules and package to be tested in different processes, using nose | ||
or trial where appropriate. | ||||
2. With the regular nose syntax, like `iptest -vvs IPython`. In this form | ||||
the script simply calls nose, but with special command line flags and | ||||
plugins loaded. | ||||
Fernando Perez
|
r1574 | """ | ||
MinRK
|
r18242 | # Copyright (c) IPython Development Team. | ||
# Distributed under the terms of the Modified BSD License. | ||||
Brian Granger
|
r2498 | |||
Fernando Perez
|
r1851 | |||
MinRK
|
r7412 | import glob | ||
Thomas Kluyver
|
r12973 | from io import BytesIO | ||
Brian Granger
|
r1972 | import os | ||
import os.path as path | ||||
Fernando Perez
|
r1574 | import sys | ||
Thomas Kluyver
|
r12973 | from threading import Thread, Lock, Event | ||
Fernando Perez
|
r1574 | import warnings | ||
import nose.plugins.builtin | ||||
Thomas Kluyver
|
r6101 | from nose.plugins.xunit import Xunit | ||
from nose import SkipTest | ||||
Fernando Perez
|
r1851 | from nose.core import TestProgram | ||
Thomas Kluyver
|
r12608 | from nose.plugins import Plugin | ||
Thomas Kluyver
|
r12973 | from nose.util import safe_str | ||
Fernando Perez
|
r1574 | |||
Matthias Bussonnier
|
r21864 | from IPython import version_info | ||
Min RK
|
r18692 | from IPython.utils.py3compat import bytes_to_str | ||
MinRK
|
r4857 | from IPython.utils.importstring import import_item | ||
Fernando Perez
|
r2480 | from IPython.testing.plugin.ipdoctest import IPythonDoctest | ||
Thomas Kluyver
|
r6101 | from IPython.external.decorators import KnownFailure, knownfailureif | ||
Fernando Perez
|
r1574 | |||
Brian Granger
|
r1979 | pjoin = path.join | ||
Fernando Perez
|
r2483 | |||
Matthias Bussonnier
|
r21864 | # Enable printing all warnings raise by IPython's modules | ||
Matthias Bussonnier
|
r22379 | warnings.filterwarnings('ignore', message='.*Matplotlib is building the font cache.*', category=UserWarning, module='.*') | ||
Paul Ivanov
|
r22959 | warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*') | ||
Matthias Bussonnier
|
r22497 | warnings.filterwarnings('error', message=".*{'config': True}.*", category=DeprecationWarning, module='IPy.*') | ||
Matthias Bussonnier
|
r21864 | warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*') | ||
Matthias Bussonnier
|
r22990 | warnings.filterwarnings('error', message='.*apply_wrapper.*', category=DeprecationWarning, module='.*') | ||
warnings.filterwarnings('error', message='.*make_label_dec', category=DeprecationWarning, module='.*') | ||||
warnings.filterwarnings('error', message='.*decorated_dummy.*', category=DeprecationWarning, module='.*') | ||||
warnings.filterwarnings('error', message='.*skip_file_no_x11.*', category=DeprecationWarning, module='.*') | ||||
warnings.filterwarnings('error', message='.*onlyif_any_cmd_exists.*', category=DeprecationWarning, module='.*') | ||||
Matthias Bussonnier
|
r22992 | |||
warnings.filterwarnings('error', message='.*disable_gui.*', category=DeprecationWarning, module='.*') | ||||
Matthias Bussonnier
|
r22993 | warnings.filterwarnings('error', message='.*ExceptionColors global is deprecated.*', category=DeprecationWarning, module='.*') | ||
Matthias Bussonnier
|
r23284 | # Jedi older versions | ||
warnings.filterwarnings( | ||||
'error', message='.*elementwise != comparison failed and.*', category=FutureWarning, module='.*') | ||||
Matthias Bussonnier
|
r21864 | if version_info < (6,): | ||
# nose.tools renames all things from `camelCase` to `snake_case` which raise an | ||||
# warning with the runner they also import from standard import library. (as of Dec 2015) | ||||
# Ignore, let's revisit that in a couple of years for IPython 6. | ||||
Matthias Bussonnier
|
r23284 | warnings.filterwarnings( | ||
'ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*') | ||||
if version_info < (7,): | ||||
warnings.filterwarnings('ignore', message='.*Completer.complete.*', | ||||
category=PendingDeprecationWarning, module='.*') | ||||
else: | ||||
warnings.warn( | ||||
'Completer.complete was pending deprecation and should be changed to Deprecated', FutureWarning) | ||||
Matthias Bussonnier
|
r21864 | |||
Thomas Kluyver
|
r6101 | # ------------------------------------------------------------------------------ | ||
# Monkeypatch Xunit to count known failures as skipped. | ||||
# ------------------------------------------------------------------------------ | ||||
Thomas Kluyver
|
r6102 | def monkeypatch_xunit(): | ||
Thomas Kluyver
|
r6101 | try: | ||
knownfailureif(True)(lambda: None)() | ||||
except Exception as e: | ||||
KnownFailureTest = type(e) | ||||
def addError(self, test, err, capt=None): | ||||
if issubclass(err[0], KnownFailureTest): | ||||
err = (SkipTest,) + err[1:] | ||||
return self.orig_addError(test, err, capt) | ||||
Xunit.orig_addError = Xunit.addError | ||||
Xunit.addError = addError | ||||
Fernando Perez
|
r2398 | #----------------------------------------------------------------------------- | ||
Thomas Kluyver
|
r12608 | # Check which dependencies are installed and greater than minimum version. | ||
Fernando Perez
|
r1851 | #----------------------------------------------------------------------------- | ||
MinRK
|
r4857 | def extract_version(mod): | ||
return mod.__version__ | ||||
Fernando Perez
|
r1851 | |||
MinRK
|
r4857 | def test_for(item, min_version=None, callback=extract_version): | ||
"""Test to see if item is importable, and optionally check against a minimum | ||||
version. | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4857 | If min_version is given, the default behavior is to check against the | ||
`__version__` attribute of the item, but specifying `callback` allows you to | ||||
extract the value you are interested in. e.g:: | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4857 | In [1]: import sys | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4857 | In [2]: from IPython.testing.iptest import test_for | ||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4857 | In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info) | ||
Out[3]: True | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4857 | """ | ||
Administrator
|
r1981 | try: | ||
MinRK
|
r4857 | check = import_item(item) | ||
Fernando Perez
|
r2488 | except (ImportError, RuntimeError): | ||
MinRK
|
r4857 | # GTK reports Runtime error if it can't be initialized even if it's | ||
Fernando Perez
|
r2488 | # importable. | ||
Administrator
|
r1981 | return False | ||
else: | ||||
MinRK
|
r3640 | if min_version: | ||
MinRK
|
r4857 | if callback: | ||
# extra processing step to get version to compare | ||||
check = callback(check) | ||||
Bernardo B. Marques
|
r4872 | |||
MinRK
|
r4857 | return check >= min_version | ||
MinRK
|
r3640 | else: | ||
return True | ||||
Administrator
|
r1981 | |||
Fernando Perez
|
r2496 | # Global dict where we can store information on what we have and what we don't | ||
# have available at test run time | ||||
Rémy Léone
|
r21781 | have = {'matplotlib': test_for('matplotlib'), | ||
'pygments': test_for('pygments'), | ||||
'sqlite3': test_for('sqlite3')} | ||||
Administrator
|
r1981 | |||
Fernando Perez
|
r2494 | #----------------------------------------------------------------------------- | ||
Thomas Kluyver
|
r12608 | # Test suite definitions | ||
Fernando Perez
|
r2494 | #----------------------------------------------------------------------------- | ||
Administrator
|
r1980 | |||
Min RK
|
r21225 | test_group_names = ['core', | ||
Thomas Kluyver
|
r12608 | 'extensions', 'lib', 'terminal', 'testing', 'utils', | ||
] | ||||
class TestSection(object): | ||||
def __init__(self, name, includes): | ||||
self.name = name | ||||
self.includes = includes | ||||
self.excludes = [] | ||||
self.dependencies = [] | ||||
self.enabled = True | ||||
def exclude(self, module): | ||||
if not module.startswith('IPython'): | ||||
module = self.includes[0] + "." + module | ||||
self.excludes.append(module.replace('.', os.sep)) | ||||
def requires(self, *packages): | ||||
self.dependencies.extend(packages) | ||||
@property | ||||
def will_run(self): | ||||
return self.enabled and all(have[p] for p in self.dependencies) | ||||
# Name -> (include, exclude, dependencies_met) | ||||
Min RK
|
r21246 | test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names} | ||
Min RK
|
r20861 | |||
Thomas Kluyver
|
r12608 | |||
# Exclusions and dependencies | ||||
# --------------------------- | ||||
# core: | ||||
sec = test_sections['core'] | ||||
if not have['sqlite3']: | ||||
sec.exclude('tests.test_history') | ||||
sec.exclude('history') | ||||
if not have['matplotlib']: | ||||
sec.exclude('pylabtools'), | ||||
sec.exclude('tests.test_pylabtools') | ||||
# lib: | ||||
sec = test_sections['lib'] | ||||
Min RK
|
r21247 | sec.exclude('kernel') | ||
Min RK
|
r21233 | if not have['pygments']: | ||
sec.exclude('tests.test_lexers') | ||||
Thomas Kluyver
|
r12608 | # We do this unconditionally, so that the test suite doesn't import | ||
# gtk, changing the default encoding and masking some unicode bugs. | ||||
sec.exclude('inputhookgtk') | ||||
Thomas Kluyver
|
r13757 | # We also do this unconditionally, because wx can interfere with Unix signals. | ||
# There are currently no tests for it anyway. | ||||
sec.exclude('inputhookwx') | ||||
Thomas Kluyver
|
r12608 | # Testing inputhook will need a lot of thought, to figure out | ||
# how to have tests that don't lock up with the gui event | ||||
# loops in the picture | ||||
sec.exclude('inputhook') | ||||
# testing: | ||||
Thomas Kluyver
|
r12777 | sec = test_sections['testing'] | ||
# These have to be skipped on win32 because they use echo, rm, cd, etc. | ||||
Thomas Kluyver
|
r12608 | # See ticket https://github.com/ipython/ipython/issues/87 | ||
if sys.platform == 'win32': | ||||
sec.exclude('plugin.test_exampleip') | ||||
sec.exclude('plugin.dtexample') | ||||
Min RK
|
r21227 | # don't run jupyter_console tests found via shim | ||
test_sections['terminal'].exclude('console') | ||||
Thomas Kluyver
|
r12608 | |||
# extensions: | ||||
sec = test_sections['extensions'] | ||||
Thomas Kluyver
|
r17610 | # This is deprecated in favour of rpy2 | ||
sec.exclude('rmagic') | ||||
Thomas Kluyver
|
r12608 | # autoreload does some strange stuff, so move it to its own test section | ||
sec.exclude('autoreload') | ||||
sec.exclude('tests.test_autoreload') | ||||
test_sections['autoreload'] = TestSection('autoreload', | ||||
['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload']) | ||||
test_group_names.append('autoreload') | ||||
Thomas Kluyver
|
r16981 | |||
Thomas Kluyver
|
r12608 | #----------------------------------------------------------------------------- | ||
# Functions and classes | ||||
#----------------------------------------------------------------------------- | ||||
Brian Granger
|
r2512 | |||
Thomas Kluyver
|
r12608 | def check_exclusions_exist(): | ||
Min RK
|
r21253 | from IPython.paths import get_ipython_package_dir | ||
Pierre Gerold
|
r22092 | from warnings import warn | ||
Thomas Kluyver
|
r12608 | parent = os.path.dirname(get_ipython_package_dir()) | ||
for sec in test_sections: | ||||
for pattern in sec.exclusions: | ||||
fullpath = pjoin(parent, pattern) | ||||
if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'): | ||||
warn("Excluding nonexistent file: %r" % pattern) | ||||
Brian Granger
|
r2512 | |||
Thomas Kluyver
|
r12608 | |||
class ExclusionPlugin(Plugin): | ||||
"""A nose plugin to effect our exclusions of files and directories. | ||||
Fernando Perez
|
r2454 | """ | ||
Thomas Kluyver
|
r12608 | name = 'exclusions' | ||
score = 3000 # Should come before any other plugins | ||||
MinRK
|
r9375 | |||
Thomas Kluyver
|
r12608 | def __init__(self, exclude_patterns=None): | ||
""" | ||||
Parameters | ||||
---------- | ||||
exclude_patterns : sequence of strings, optional | ||||
Thomas Kluyver
|
r12756 | Filenames containing these patterns (as raw strings, not as regular | ||
expressions) are excluded from the tests. | ||||
Thomas Kluyver
|
r12608 | """ | ||
Thomas Kluyver
|
r12756 | self.exclude_patterns = exclude_patterns or [] | ||
Thomas Kluyver
|
r12608 | super(ExclusionPlugin, self).__init__() | ||
def options(self, parser, env=os.environ): | ||||
Plugin.options(self, parser, env) | ||||
MinRK
|
r7121 | |||
Thomas Kluyver
|
r12608 | def configure(self, options, config): | ||
Plugin.configure(self, options, config) | ||||
# Override nose trying to disable plugin. | ||||
self.enabled = True | ||||
def wantFile(self, filename): | ||||
"""Return whether the given filename should be scanned for tests. | ||||
""" | ||||
Thomas Kluyver
|
r12756 | if any(pat in filename for pat in self.exclude_patterns): | ||
Thomas Kluyver
|
r12608 | return False | ||
return None | ||||
def wantDirectory(self, directory): | ||||
"""Return whether the given directory should be scanned for tests. | ||||
""" | ||||
Thomas Kluyver
|
r12756 | if any(pat in directory for pat in self.exclude_patterns): | ||
Thomas Kluyver
|
r12608 | return False | ||
return None | ||||
Fernando Perez
|
r2398 | |||
Thomas Kluyver
|
r12973 | class StreamCapturer(Thread): | ||
Thomas Kluyver
|
r13517 | daemon = True # Don't hang if main thread crashes | ||
Thomas Kluyver
|
r12973 | started = False | ||
Min RK
|
r18692 | def __init__(self, echo=False): | ||
Thomas Kluyver
|
r12973 | super(StreamCapturer, self).__init__() | ||
Min RK
|
r18692 | self.echo = echo | ||
Thomas Kluyver
|
r12973 | self.streams = [] | ||
self.buffer = BytesIO() | ||||
Thomas Kluyver
|
r13405 | self.readfd, self.writefd = os.pipe() | ||
Thomas Kluyver
|
r12973 | self.buffer_lock = Lock() | ||
self.stop = Event() | ||||
def run(self): | ||||
self.started = True | ||||
Thomas Kluyver
|
r13405 | |||
Thomas Kluyver
|
r12973 | while not self.stop.is_set(): | ||
Thomas Kluyver
|
r13423 | chunk = os.read(self.readfd, 1024) | ||
Thomas Kluyver
|
r13405 | |||
Thomas Kluyver
|
r13423 | with self.buffer_lock: | ||
self.buffer.write(chunk) | ||||
Min RK
|
r18692 | if self.echo: | ||
sys.stdout.write(bytes_to_str(chunk)) | ||||
Thomas Kluyver
|
r12973 | |||
Thomas Kluyver
|
r13405 | os.close(self.readfd) | ||
os.close(self.writefd) | ||||
Thomas Kluyver
|
r13423 | |||
Thomas Kluyver
|
r12973 | def reset_buffer(self): | ||
with self.buffer_lock: | ||||
self.buffer.truncate(0) | ||||
self.buffer.seek(0) | ||||
Thomas Kluyver
|
r13423 | |||
Thomas Kluyver
|
r12973 | def get_buffer(self): | ||
with self.buffer_lock: | ||||
return self.buffer.getvalue() | ||||
Thomas Kluyver
|
r13423 | |||
Thomas Kluyver
|
r12973 | def ensure_started(self): | ||
if not self.started: | ||||
self.start() | ||||
Thomas Kluyver
|
r13423 | def halt(self): | ||
"""Safely stop the thread.""" | ||||
if not self.started: | ||||
return | ||||
self.stop.set() | ||||
Min RK
|
r20318 | os.write(self.writefd, b'\0') # Ensure we're not locked in a read() | ||
Thomas Kluyver
|
r13423 | self.join() | ||
Thomas Kluyver
|
r12973 | class SubprocessStreamCapturePlugin(Plugin): | ||
name='subprocstreams' | ||||
def __init__(self): | ||||
Plugin.__init__(self) | ||||
self.stream_capturer = StreamCapturer() | ||||
Thomas Kluyver
|
r13824 | self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture') | ||
Thomas Kluyver
|
r12973 | # This is ugly, but distant parts of the test machinery need to be able | ||
Thomas Kluyver
|
r13405 | # to redirect streams, so we make the object globally accessible. | ||
Thomas Kluyver
|
r13824 | nose.iptest_stdstreams_fileno = self.get_write_fileno | ||
def get_write_fileno(self): | ||||
if self.destination == 'capture': | ||||
self.stream_capturer.ensure_started() | ||||
return self.stream_capturer.writefd | ||||
elif self.destination == 'discard': | ||||
return os.open(os.devnull, os.O_WRONLY) | ||||
else: | ||||
return sys.__stdout__.fileno() | ||||
Thomas Kluyver
|
r12973 | |||
def configure(self, options, config): | ||||
Plugin.configure(self, options, config) | ||||
# Override nose trying to disable plugin. | ||||
Thomas Kluyver
|
r13824 | if self.destination == 'capture': | ||
self.enabled = True | ||||
Thomas Kluyver
|
r12973 | |||
def startTest(self, test): | ||||
# Reset log capture | ||||
self.stream_capturer.reset_buffer() | ||||
def formatFailure(self, test, err): | ||||
# Show output | ||||
ec, ev, tb = err | ||||
Thomas Kluyver
|
r13155 | captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace') | ||
if captured.strip(): | ||||
ev = safe_str(ev) | ||||
out = [ev, '>> begin captured subprocess output <<', | ||||
captured, | ||||
'>> end captured subprocess output <<'] | ||||
return ec, '\n'.join(out), tb | ||||
return err | ||||
Thomas Kluyver
|
r12973 | |||
formatError = formatFailure | ||||
def finalize(self, result): | ||||
Thomas Kluyver
|
r13423 | self.stream_capturer.halt() | ||
Thomas Kluyver
|
r12973 | |||
Brian Granger
|
r1972 | def run_iptest(): | ||
"""Run the IPython test suite using nose. | ||||
Bernardo B. Marques
|
r4872 | |||
Brian Granger
|
r1972 | This function is called when this script is **not** called with the form | ||
`iptest all`. It simply calls nose with appropriate command line flags | ||||
and accepts all of the standard nose arguments. | ||||
Fernando Perez
|
r1574 | """ | ||
Thomas Kluyver
|
r6102 | # Apply our monkeypatch to Xunit | ||
Thomas Kluyver
|
r6113 | if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'): | ||
Thomas Kluyver
|
r6102 | monkeypatch_xunit() | ||
Fernando Perez
|
r1574 | |||
Thomas Kluyver
|
r12622 | arg1 = sys.argv[1] | ||
if arg1 in test_sections: | ||||
section = test_sections[arg1] | ||||
sys.argv[1:2] = section.includes | ||||
elif arg1.startswith('IPython.') and arg1[8:] in test_sections: | ||||
section = test_sections[arg1[8:]] | ||||
Thomas Kluyver
|
r12620 | sys.argv[1:2] = section.includes | ||
else: | ||||
section = TestSection(arg1, includes=[arg1]) | ||||
Fernando Perez
|
r1574 | |||
Fernando Perez
|
r2487 | argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks | ||
Fernando Perez
|
r1761 | # We add --exe because of setuptools' imbecility (it | ||
# blindly does chmod +x on ALL files). Nose does the | ||||
# right thing and it tries to avoid executables, | ||||
# setuptools unfortunately forces our hand here. This | ||||
# has been discussed on the distutils list and the | ||||
# setuptools devs refuse to fix this problem! | ||||
'--exe', | ||||
] | ||||
MinRK
|
r8201 | if '-a' not in argv and '-A' not in argv: | ||
argv = argv + ['-a', '!crash'] | ||||
Fernando Perez
|
r1574 | |||
Fernando Perez
|
r2494 | if nose.__version__ >= '0.11': | ||
# I don't fully understand why we need this one, but depending on what | ||||
# directory the test suite is run from, if we don't give it, 0 tests | ||||
# get run. Specifically, if the test suite is run from the source dir | ||||
# with an argument (like 'iptest.py IPython.core', 0 tests are run, | ||||
# even if the same call done in this directory works fine). It appears | ||||
# that if the requested package is in the current dir, nose bails early | ||||
# by default. Since it's otherwise harmless, leave it in by default | ||||
# for nose >= 0.11, though unfortunately nose 0.10 doesn't support it. | ||||
argv.append('--traverse-namespace') | ||||
Fernando Perez
|
r1574 | |||
Min RK
|
r21133 | plugins = [ ExclusionPlugin(section.excludes), KnownFailure(), | ||
Thomas Kluyver
|
r12973 | SubprocessStreamCapturePlugin() ] | ||
Thomas Kluyver
|
r12608 | |||
Min RK
|
r21133 | # we still have some vestigial doctests in core | ||
Paul Ivanov
|
r22979 | if (section.name.startswith(('core', 'IPython.core', 'IPython.utils'))): | ||
Min RK
|
r21133 | plugins.append(IPythonDoctest()) | ||
argv.extend([ | ||||
'--with-ipdoctest', | ||||
'--ipdoctest-tests', | ||||
'--ipdoctest-extension=txt', | ||||
]) | ||||
Thomas Kluyver
|
r12608 | # Use working directory set by parent process (see iptestcontroller) | ||
if 'IPTEST_WORKING_DIR' in os.environ: | ||||
os.chdir(os.environ['IPTEST_WORKING_DIR']) | ||||
Fernando Perez
|
r8489 | |||
# We need a global ipython running in this process, but the special | ||||
# in-process group spawns its own IPython kernels, so for *that* group we | ||||
# must avoid also opening the global one (otherwise there's a conflict of | ||||
# singletons). Ultimately the solution to this problem is to refactor our | ||||
# assumptions about what needs to be a singleton and what doesn't (app | ||||
# objects should, individual shells shouldn't). But for now, this | ||||
# workaround allows the test suite for the inprocess module to complete. | ||||
Thomas Kluyver
|
r12620 | if 'kernel.inprocess' not in section.name: | ||
Thomas Kluyver
|
r12612 | from IPython.testing import globalipapp | ||
Fernando Perez
|
r8489 | globalipapp.start_ipython() | ||
Fernando Perez
|
r2414 | # Now nose can run | ||
Matthew Brett
|
r4567 | TestProgram(argv=argv, addplugins=plugins) | ||
Brian Granger
|
r1972 | |||
if __name__ == '__main__': | ||||
Thomas Kluyver
|
r12607 | run_iptest() | ||