iptest.py
420 lines
| 15.1 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 | ||||
calling this script (with different arguments) or trial 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. | ||||
For now, this script requires that both nose and twisted are installed. This | ||||
will change in the future. | ||||
Fernando Perez
|
r1574 | """ | ||
Fernando Perez
|
r2414 | from __future__ import absolute_import | ||
Fernando Perez
|
r1851 | #----------------------------------------------------------------------------- | ||
# Module imports | ||||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r2442 | # Stdlib | ||
Brian Granger
|
r1972 | import os | ||
import os.path as path | ||||
Fernando Perez
|
r2399 | import signal | ||
Fernando Perez
|
r1574 | import sys | ||
Brian Granger
|
r1972 | import subprocess | ||
Fernando Perez
|
r2111 | import tempfile | ||
Brian Granger
|
r1972 | import time | ||
Fernando Perez
|
r1574 | import warnings | ||
Fernando Perez
|
r2442 | # Note: monkeypatch! | ||
# We need to monkeypatch a small problem in nose itself first, before importing | ||||
# it for actual use. This should get into nose upstream, but its release cycle | ||||
# is slow and we need it for our parametric tests to work correctly. | ||||
from . import nosepatch | ||||
# Now, proceed to import nose itself | ||||
Fernando Perez
|
r1574 | import nose.plugins.builtin | ||
Fernando Perez
|
r1851 | from nose.core import TestProgram | ||
Fernando Perez
|
r1574 | |||
Fernando Perez
|
r2442 | # Our own imports | ||
Fernando Perez
|
r2398 | from IPython.utils import genutils | ||
from IPython.utils.platutils import find_cmd, FindCmdError | ||||
Fernando Perez
|
r2414 | from . import globalipapp | ||
Fernando Perez
|
r2461 | from . import tools | ||
Fernando Perez
|
r2414 | from .plugin.ipdoctest import IPythonDoctest | ||
Fernando Perez
|
r1574 | |||
Brian Granger
|
r1979 | pjoin = path.join | ||
Fernando Perez
|
r1851 | #----------------------------------------------------------------------------- | ||
Fernando Perez
|
r2398 | # Warnings control | ||
#----------------------------------------------------------------------------- | ||||
# Twisted generates annoying warnings with Python 2.6, as will do other code | ||||
# that imports 'sets' as of today | ||||
warnings.filterwarnings('ignore', 'the sets module is deprecated', | ||||
DeprecationWarning ) | ||||
Fernando Perez
|
r2461 | # This one also comes from Twisted | ||
warnings.filterwarnings('ignore', 'the sha module is deprecated', | ||||
DeprecationWarning) | ||||
Fernando Perez
|
r2398 | #----------------------------------------------------------------------------- | ||
Administrator
|
r1981 | # Logic for skipping doctests | ||
Fernando Perez
|
r1851 | #----------------------------------------------------------------------------- | ||
Administrator
|
r1981 | def test_for(mod): | ||
"""Test to see if mod is importable.""" | ||||
try: | ||||
__import__(mod) | ||||
except ImportError: | ||||
return False | ||||
else: | ||||
return True | ||||
Fernando Perez
|
r2455 | |||
Administrator
|
r1981 | have_curses = test_for('_curses') | ||
have_wx = test_for('wx') | ||||
Fernando Perez
|
r2091 | have_wx_aui = test_for('wx.aui') | ||
Administrator
|
r1981 | have_zi = test_for('zope.interface') | ||
have_twisted = test_for('twisted') | ||||
have_foolscap = test_for('foolscap') | ||||
have_objc = test_for('objc') | ||||
have_pexpect = test_for('pexpect') | ||||
Brian Granger
|
r2083 | have_gtk = test_for('gtk') | ||
have_gobject = test_for('gobject') | ||||
Administrator
|
r1981 | |||
Administrator
|
r1980 | |||
Brian Granger
|
r2079 | def make_exclude(): | ||
Fernando Perez
|
r2454 | """Make patterns of modules and packages to exclude from testing. | ||
For the IPythonDoctest plugin, we need to exclude certain patterns that | ||||
cause testing problems. We should strive to minimize the number of | ||||
skipped modules, since this means untested code. As the testing | ||||
machinery solidifies, this list should eventually become empty. | ||||
These modules and packages will NOT get scanned by nose at all for tests. | ||||
""" | ||||
# Simple utility to make IPython paths more readably, we need a lot of | ||||
# these below | ||||
ipjoin = lambda *paths: pjoin('IPython', *paths) | ||||
exclusions = [ipjoin('external'), | ||||
ipjoin('frontend', 'process', 'winprocess.py'), | ||||
Fernando Perez
|
r2417 | pjoin('IPython_doctest_plugin'), | ||
Fernando Perez
|
r2454 | ipjoin('quarantine'), | ||
ipjoin('deathrow'), | ||||
ipjoin('testing', 'attic'), | ||||
Fernando Perez
|
r2461 | # This guy is probably attic material | ||
Fernando Perez
|
r2454 | ipjoin('testing', 'mkdoctests'), | ||
Fernando Perez
|
r2461 | # 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 | ||||
Fernando Perez
|
r2454 | ipjoin('lib', 'inputhook'), | ||
Fernando Perez
|
r2417 | # Config files aren't really importable stand-alone | ||
Fernando Perez
|
r2454 | ipjoin('config', 'default'), | ||
ipjoin('config', 'profile'), | ||||
Fernando Perez
|
r2417 | ] | ||
Brian Granger
|
r2079 | |||
if not have_wx: | ||||
Fernando Perez
|
r2454 | exclusions.append(ipjoin('gui')) | ||
exclusions.append(ipjoin('frontend', 'wx')) | ||||
exclusions.append(ipjoin('lib', 'inputhookwx')) | ||||
Brian Granger
|
r2083 | |||
if not have_gtk or not have_gobject: | ||||
Fernando Perez
|
r2454 | exclusions.append(ipjoin('lib', 'inputhookgtk')) | ||
Brian Granger
|
r2079 | |||
Brian Granger
|
r2124 | if not have_wx_aui: | ||
Fernando Perez
|
r2454 | exclusions.append(ipjoin('gui', 'wx', 'wxIPython')) | ||
Brian Granger
|
r2124 | |||
Brian Granger
|
r2079 | if not have_objc: | ||
Fernando Perez
|
r2454 | exclusions.append(ipjoin('frontend', 'cocoa')) | ||
Brian Granger
|
r2079 | |||
if not sys.platform == 'win32': | ||||
Fernando Perez
|
r2454 | exclusions.append(ipjoin('utils', 'platutils_win32')) | ||
Brian Granger
|
r2079 | |||
# These have to be skipped on win32 because the use echo, rm, cd, etc. | ||||
# See ticket https://bugs.launchpad.net/bugs/366982 | ||||
if sys.platform == 'win32': | ||||
Fernando Perez
|
r2454 | exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip')) | ||
exclusions.append(ipjoin('testing', 'plugin', 'dtexample')) | ||||
Brian Granger
|
r2079 | |||
if not os.name == 'posix': | ||||
Fernando Perez
|
r2454 | exclusions.append(ipjoin('utils', 'platutils_posix')) | ||
Brian Granger
|
r2079 | |||
if not have_pexpect: | ||||
Fernando Perez
|
r2455 | exclusions.extend([ipjoin('scripts', 'irunner'), | ||
ipjoin('lib', 'irunner')]) | ||||
Brian Granger
|
r2079 | |||
Brian Granger
|
r2150 | # This is scary. We still have things in frontend and testing that | ||
# are being tested by nose that use twisted. We need to rethink | ||||
# how we are isolating dependencies in testing. | ||||
if not (have_twisted and have_zi and have_foolscap): | ||||
Fernando Perez
|
r2454 | exclusions.extend( | ||
[ipjoin('frontend', 'asyncfrontendbase'), | ||||
ipjoin('frontend', 'prefilterfrontend'), | ||||
ipjoin('frontend', 'frontendbase'), | ||||
ipjoin('frontend', 'linefrontendbase'), | ||||
ipjoin('frontend', 'tests', 'test_linefrontend'), | ||||
ipjoin('frontend', 'tests', 'test_frontendbase'), | ||||
ipjoin('frontend', 'tests', 'test_prefilterfrontend'), | ||||
ipjoin('frontend', 'tests', 'test_asyncfrontendbase'), | ||||
ipjoin('testing', 'parametric'), | ||||
ipjoin('testing', 'util'), | ||||
ipjoin('testing', 'tests', 'test_decorators_trial'), | ||||
] ) | ||||
Brian Granger
|
r2150 | |||
Brian Granger
|
r2079 | # This is needed for the reg-exp to match on win32 in the ipdoctest plugin. | ||
if sys.platform == 'win32': | ||||
Fernando Perez
|
r2414 | exclusions = [s.replace('\\','\\\\') for s in exclusions] | ||
Brian Granger
|
r2079 | |||
Fernando Perez
|
r2414 | return exclusions | ||
Administrator
|
r1980 | |||
Fernando Perez
|
r1851 | #----------------------------------------------------------------------------- | ||
# Functions and classes | ||||
#----------------------------------------------------------------------------- | ||||
Fernando Perez
|
r2398 | class IPTester(object): | ||
"""Call that calls iptest or trial in a subprocess. | ||||
""" | ||||
Fernando Perez
|
r2399 | #: string, name of test runner that will be called | ||
runner = None | ||||
#: list, parameters for test runner | ||||
params = None | ||||
#: list, arguments of system call to be made to call test runner | ||||
call_args = None | ||||
#: list, process ids of subprocesses we start (for cleanup) | ||||
pids = None | ||||
Fernando Perez
|
r2398 | def __init__(self,runner='iptest',params=None): | ||
Fernando Perez
|
r2399 | """Create new test runner.""" | ||
Fernando Perez
|
r2398 | if runner == 'iptest': | ||
# Find our own 'iptest' script OS-level entry point | ||||
try: | ||||
Fernando Perez
|
r2414 | iptest_path = os.path.abspath(find_cmd('iptest')) | ||
Fernando Perez
|
r2398 | except FindCmdError: | ||
# Script not installed (may be the case for testing situations | ||||
# that are running from a source tree only), pull from internal | ||||
# path: | ||||
Fernando Perez
|
r2465 | pak_dir = os.path.abspath(genutils.get_ipython_package_dir()) | ||
iptest_path = pjoin(pak_dir, 'scripts', 'iptest') | ||||
Fernando Perez
|
r2461 | self.runner = tools.cmd2argv(iptest_path) + ['-v'] | ||
Fernando Perez
|
r2398 | else: | ||
Fernando Perez
|
r2461 | self.runner = tools.cmd2argv(os.path.abspath(find_cmd('trial'))) | ||
Fernando Perez
|
r2398 | if params is None: | ||
params = [] | ||||
if isinstance(params,str): | ||||
params = [params] | ||||
self.params = params | ||||
# Assemble call | ||||
self.call_args = self.runner+self.params | ||||
Fernando Perez
|
r2399 | # Store pids of anything we start to clean up on deletion, if possible | ||
# (on posix only, since win32 has no os.kill) | ||||
self.pids = [] | ||||
Fernando Perez
|
r2398 | if sys.platform == 'win32': | ||
def _run_cmd(self): | ||||
# On Windows, use os.system instead of subprocess.call, because I | ||||
# was having problems with subprocess and I just don't know enough | ||||
# about win32 to debug this reliably. Os.system may be the 'old | ||||
# fashioned' way to do it, but it works just fine. If someone | ||||
# later can clean this up that's fine, as long as the tests run | ||||
# reliably in win32. | ||||
return os.system(' '.join(self.call_args)) | ||||
else: | ||||
def _run_cmd(self): | ||||
Fernando Perez
|
r2399 | subp = subprocess.Popen(self.call_args) | ||
self.pids.append(subp.pid) | ||||
# If this fails, the pid will be left in self.pids and cleaned up | ||||
# later, but if the wait call succeeds, then we can clear the | ||||
# stored pid. | ||||
retcode = subp.wait() | ||||
self.pids.pop() | ||||
return retcode | ||||
Fernando Perez
|
r2398 | |||
def run(self): | ||||
"""Run the stored commands""" | ||||
try: | ||||
return self._run_cmd() | ||||
except: | ||||
import traceback | ||||
traceback.print_exc() | ||||
return 1 # signal failure | ||||
Fernando Perez
|
r2399 | def __del__(self): | ||
"""Cleanup on exit by killing any leftover processes.""" | ||||
if not hasattr(os, 'kill'): | ||||
return | ||||
for pid in self.pids: | ||||
try: | ||||
print 'Cleaning stale PID:', pid | ||||
os.kill(pid, signal.SIGKILL) | ||||
except OSError: | ||||
# This is just a best effort, if we fail or the process was | ||||
# really gone, ignore it. | ||||
Fernando Perez
|
r2400 | pass | ||
Fernando Perez
|
r2398 | |||
def make_runners(): | ||||
"""Define the top-level packages that need to be tested. | ||||
""" | ||||
nose_packages = ['config', 'core', 'extensions', 'frontend', 'lib', | ||||
Fernando Perez
|
r2464 | 'scripts', 'testing', 'utils', | ||
# Note that we list the kernel here, though the bulk of it | ||||
# is twisted-based, because nose picks up doctests that | ||||
# twisted doesn't. | ||||
'kernel'] | ||||
Fernando Perez
|
r2398 | trial_packages = ['kernel'] | ||
if have_wx: | ||||
nose_packages.append('gui') | ||||
Fernando Perez
|
r2414 | #nose_packages = ['core'] # dbg | ||
#trial_packages = [] # dbg | ||||
Fernando Perez
|
r2398 | nose_packages = ['IPython.%s' % m for m in nose_packages ] | ||
trial_packages = ['IPython.%s' % m for m in trial_packages ] | ||||
# Make runners, most with nose | ||||
nose_testers = [IPTester(params=v) for v in nose_packages] | ||||
runners = dict(zip(nose_packages, nose_testers)) | ||||
# And add twisted ones if conditions are met | ||||
if have_zi and have_twisted and have_foolscap: | ||||
trial_testers = [IPTester('trial',params=v) for v in trial_packages] | ||||
runners.update(dict(zip(trial_packages,trial_testers))) | ||||
return runners | ||||
Brian Granger
|
r1972 | 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. | ||||
Fernando Perez
|
r1574 | """ | ||
warnings.filterwarnings('ignore', | ||||
'This will be removed soon. Use IPython.testing.util instead') | ||||
Fernando Perez
|
r2414 | argv = sys.argv + [ '--detailed-errors', | ||
# Loading ipdoctest causes problems with Twisted, but | ||||
# our test suite runner now separates things and runs | ||||
# all Twisted tests with trial. | ||||
'--with-ipdoctest', | ||||
'--ipdoctest-tests','--ipdoctest-extension=txt', | ||||
#'-x','-s', # dbg | ||||
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', | ||||
] | ||||
Fernando Perez
|
r1574 | |||
Fernando Perez
|
r1848 | # Detect if any tests were required by explicitly calling an IPython | ||
# submodule or giving a specific path | ||||
has_tests = False | ||||
Fernando Perez
|
r1574 | for arg in sys.argv: | ||
Fernando Perez
|
r1848 | if 'IPython' in arg or arg.endswith('.py') or \ | ||
(':' in arg and '.py' in arg): | ||||
has_tests = True | ||||
Fernando Perez
|
r1574 | break | ||
Fernando Perez
|
r1910 | |||
Fernando Perez
|
r1848 | # If nothing was specifically requested, test full IPython | ||
if not has_tests: | ||||
Fernando Perez
|
r1574 | argv.append('IPython') | ||
Fernando Perez
|
r2414 | ## # Construct list of plugins, omitting the existing doctest plugin, which | ||
## # ours replaces (and extends). | ||||
plugins = [IPythonDoctest(make_exclude())] | ||||
Fernando Perez
|
r1761 | for p in nose.plugins.builtin.plugins: | ||
plug = p() | ||||
if plug.name == 'doctest': | ||||
continue | ||||
plugins.append(plug) | ||||
Fernando Perez
|
r2414 | # We need a global ipython running in this process | ||
globalipapp.start_ipython() | ||||
# Now nose can run | ||||
Fernando Perez
|
r1574 | TestProgram(argv=argv,plugins=plugins) | ||
Brian Granger
|
r1972 | |||
def run_iptestall(): | ||||
"""Run the entire IPython test suite by calling nose and trial. | ||||
This function constructs :class:`IPTester` instances for all IPython | ||||
modules and package and then runs each of them. This causes the modules | ||||
and packages of IPython to be tested each in their own subprocess using | ||||
nose or twisted.trial appropriately. | ||||
""" | ||||
Brian Granger
|
r2079 | |||
Brian Granger
|
r1972 | runners = make_runners() | ||
Brian Granger
|
r2079 | |||
Fernando Perez
|
r2398 | # Run the test runners in a temporary dir so we can nuke it when finished | ||
# to clean up any junk files left over by accident. This also makes it | ||||
# robust against being run in non-writeable directories by mistake, as the | ||||
# temp dir will always be user-writeable. | ||||
curdir = os.getcwd() | ||||
testdir = tempfile.gettempdir() | ||||
os.chdir(testdir) | ||||
Brian Granger
|
r1972 | # Run all test runners, tracking execution time | ||
failed = {} | ||||
t_start = time.time() | ||||
Fernando Perez
|
r2398 | try: | ||
for name,runner in runners.iteritems(): | ||||
print '*'*77 | ||||
print 'IPython test group:',name | ||||
res = runner.run() | ||||
if res: | ||||
failed[name] = res | ||||
finally: | ||||
os.chdir(curdir) | ||||
Brian Granger
|
r1972 | t_end = time.time() | ||
t_tests = t_end - t_start | ||||
nrunners = len(runners) | ||||
nfail = len(failed) | ||||
# summarize results | ||||
print '*'*77 | ||||
Fernando Perez
|
r2091 | print 'Ran %s test groups in %.3fs' % (nrunners, t_tests) | ||
Brian Granger
|
r1972 | |||
if not failed: | ||||
print 'OK' | ||||
else: | ||||
# If anything went wrong, point out what command to rerun manually to | ||||
# see the actual errors and individual summary | ||||
Fernando Perez
|
r2091 | print 'ERROR - %s out of %s test groups failed.' % (nfail, nrunners) | ||
Brian Granger
|
r1972 | for name in failed: | ||
failed_runner = runners[name] | ||||
print '-'*40 | ||||
print 'Runner failed:',name | ||||
print 'You may wish to rerun this one individually, with:' | ||||
print ' '.join(failed_runner.call_args) | ||||
def main(): | ||||
Brian Granger
|
r1990 | if len(sys.argv) == 1: | ||
Brian Granger
|
r1972 | run_iptestall() | ||
else: | ||||
Brian Granger
|
r1990 | if sys.argv[1] == 'all': | ||
run_iptestall() | ||||
else: | ||||
run_iptest() | ||||
Brian Granger
|
r1972 | |||
if __name__ == '__main__': | ||||
Fernando Perez
|
r2091 | main() | ||