diff options
author | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:24:06 +0300 |
---|---|---|
committer | nkozlovskiy <nmk@ydb.tech> | 2023-09-29 12:41:34 +0300 |
commit | e0e3e1717e3d33762ce61950504f9637a6e669ed (patch) | |
tree | bca3ff6939b10ed60c3d5c12439963a1146b9711 /contrib/python/ipython/py2/IPython/testing/iptest.py | |
parent | 38f2c5852db84c7b4d83adfcb009eb61541d1ccd (diff) | |
download | ydb-e0e3e1717e3d33762ce61950504f9637a6e669ed.tar.gz |
add ydb deps
Diffstat (limited to 'contrib/python/ipython/py2/IPython/testing/iptest.py')
-rw-r--r-- | contrib/python/ipython/py2/IPython/testing/iptest.py | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/contrib/python/ipython/py2/IPython/testing/iptest.py b/contrib/python/ipython/py2/IPython/testing/iptest.py new file mode 100644 index 0000000000..4018264125 --- /dev/null +++ b/contrib/python/ipython/py2/IPython/testing/iptest.py @@ -0,0 +1,443 @@ +# -*- coding: utf-8 -*- +"""IPython Test Suite Runner. + +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 + calling this script (with different arguments) recursively. This + 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. + +""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from __future__ import print_function + +import glob +from io import BytesIO +import os +import os.path as path +import sys +from threading import Thread, Lock, Event +import warnings + +import nose.plugins.builtin +from nose.plugins.xunit import Xunit +from nose import SkipTest +from nose.core import TestProgram +from nose.plugins import Plugin +from nose.util import safe_str + +from IPython import version_info +from IPython.utils.py3compat import bytes_to_str +from IPython.utils.importstring import import_item +from IPython.testing.plugin.ipdoctest import IPythonDoctest +from IPython.external.decorators import KnownFailure, knownfailureif + +pjoin = path.join + + +# Enable printing all warnings raise by IPython's modules +warnings.filterwarnings('ignore', message='.*Matplotlib is building the font cache.*', category=UserWarning, module='.*') +if sys.version_info > (3,0): + warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*') +warnings.filterwarnings('error', message=".*{'config': True}.*", category=DeprecationWarning, module='IPy.*') +warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*') + +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='.*') + +warnings.filterwarnings('error', message='.*disable_gui.*', category=DeprecationWarning, module='.*') + +warnings.filterwarnings('error', message='.*ExceptionColors global is deprecated.*', category=DeprecationWarning, module='.*') + +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. + warnings.filterwarnings('ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*') + + +# ------------------------------------------------------------------------------ +# Monkeypatch Xunit to count known failures as skipped. +# ------------------------------------------------------------------------------ +def monkeypatch_xunit(): + 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 + +#----------------------------------------------------------------------------- +# Check which dependencies are installed and greater than minimum version. +#----------------------------------------------------------------------------- +def extract_version(mod): + return mod.__version__ + +def test_for(item, min_version=None, callback=extract_version): + """Test to see if item is importable, and optionally check against a minimum + version. + + 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:: + + In [1]: import sys + + In [2]: from IPython.testing.iptest import test_for + + In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info) + Out[3]: True + + """ + try: + check = import_item(item) + except (ImportError, RuntimeError): + # GTK reports Runtime error if it can't be initialized even if it's + # importable. + return False + else: + if min_version: + if callback: + # extra processing step to get version to compare + check = callback(check) + + return check >= min_version + else: + return True + +# Global dict where we can store information on what we have and what we don't +# have available at test run time +have = {'matplotlib': test_for('matplotlib'), + 'pygments': test_for('pygments'), + 'sqlite3': test_for('sqlite3')} + +#----------------------------------------------------------------------------- +# Test suite definitions +#----------------------------------------------------------------------------- + +test_group_names = ['core', + '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) +test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names} + + +# 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'] +sec.exclude('kernel') +if not have['pygments']: + sec.exclude('tests.test_lexers') +# 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') +# We also do this unconditionally, because wx can interfere with Unix signals. +# There are currently no tests for it anyway. +sec.exclude('inputhookwx') +# 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: +sec = test_sections['testing'] +# These have to be skipped on win32 because they use echo, rm, cd, etc. +# See ticket https://github.com/ipython/ipython/issues/87 +if sys.platform == 'win32': + sec.exclude('plugin.test_exampleip') + sec.exclude('plugin.dtexample') + +# don't run jupyter_console tests found via shim +test_sections['terminal'].exclude('console') + +# extensions: +sec = test_sections['extensions'] +# This is deprecated in favour of rpy2 +sec.exclude('rmagic') +# 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') + + +#----------------------------------------------------------------------------- +# Functions and classes +#----------------------------------------------------------------------------- + +def check_exclusions_exist(): + from IPython.paths import get_ipython_package_dir + from warnings import warn + 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) + + +class ExclusionPlugin(Plugin): + """A nose plugin to effect our exclusions of files and directories. + """ + name = 'exclusions' + score = 3000 # Should come before any other plugins + + def __init__(self, exclude_patterns=None): + """ + Parameters + ---------- + + exclude_patterns : sequence of strings, optional + Filenames containing these patterns (as raw strings, not as regular + expressions) are excluded from the tests. + """ + self.exclude_patterns = exclude_patterns or [] + super(ExclusionPlugin, self).__init__() + + def options(self, parser, env=os.environ): + Plugin.options(self, parser, env) + + 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. + """ + if any(pat in filename for pat in self.exclude_patterns): + return False + return None + + def wantDirectory(self, directory): + """Return whether the given directory should be scanned for tests. + """ + if any(pat in directory for pat in self.exclude_patterns): + return False + return None + + +class StreamCapturer(Thread): + daemon = True # Don't hang if main thread crashes + started = False + def __init__(self, echo=False): + super(StreamCapturer, self).__init__() + self.echo = echo + self.streams = [] + self.buffer = BytesIO() + self.readfd, self.writefd = os.pipe() + self.buffer_lock = Lock() + self.stop = Event() + + def run(self): + self.started = True + + while not self.stop.is_set(): + chunk = os.read(self.readfd, 1024) + + with self.buffer_lock: + self.buffer.write(chunk) + if self.echo: + sys.stdout.write(bytes_to_str(chunk)) + + os.close(self.readfd) + os.close(self.writefd) + + def reset_buffer(self): + with self.buffer_lock: + self.buffer.truncate(0) + self.buffer.seek(0) + + def get_buffer(self): + with self.buffer_lock: + return self.buffer.getvalue() + + def ensure_started(self): + if not self.started: + self.start() + + def halt(self): + """Safely stop the thread.""" + if not self.started: + return + + self.stop.set() + os.write(self.writefd, b'\0') # Ensure we're not locked in a read() + self.join() + +class SubprocessStreamCapturePlugin(Plugin): + name='subprocstreams' + def __init__(self): + Plugin.__init__(self) + self.stream_capturer = StreamCapturer() + self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture') + # This is ugly, but distant parts of the test machinery need to be able + # to redirect streams, so we make the object globally accessible. + 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() + + def configure(self, options, config): + Plugin.configure(self, options, config) + # Override nose trying to disable plugin. + if self.destination == 'capture': + self.enabled = True + + def startTest(self, test): + # Reset log capture + self.stream_capturer.reset_buffer() + + def formatFailure(self, test, err): + # Show output + ec, ev, tb = err + 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 + + formatError = formatFailure + + def finalize(self, result): + self.stream_capturer.halt() + + +def run_iptest(): + """Run the IPython test suite using nose. + + 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. + """ + # Apply our monkeypatch to Xunit + if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'): + monkeypatch_xunit() + + 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:]] + sys.argv[1:2] = section.includes + else: + section = TestSection(arg1, includes=[arg1]) + + + argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks + # 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', + ] + if '-a' not in argv and '-A' not in argv: + argv = argv + ['-a', '!crash'] + + 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') + + plugins = [ ExclusionPlugin(section.excludes), KnownFailure(), + SubprocessStreamCapturePlugin() ] + + # we still have some vestigial doctests in core + if (section.name.startswith(('core', 'IPython.core'))): + plugins.append(IPythonDoctest()) + argv.extend([ + '--with-ipdoctest', + '--ipdoctest-tests', + '--ipdoctest-extension=txt', + ]) + + + # Use working directory set by parent process (see iptestcontroller) + if 'IPTEST_WORKING_DIR' in os.environ: + os.chdir(os.environ['IPTEST_WORKING_DIR']) + + # 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. + if 'kernel.inprocess' not in section.name: + from IPython.testing import globalipapp + globalipapp.start_ipython() + + # Now nose can run + TestProgram(argv=argv, addplugins=plugins) + +if __name__ == '__main__': + run_iptest() |