##// END OF EJS Templates
parallel is slowest of all, moving it to the front...
Paul Ivanov -
Show More
@@ -1,664 +1,664
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Suite Runner.
2 """IPython Test Suite Runner.
3
3
4 This module provides a main entry point to a user script to test IPython
4 This module provides a main entry point to a user script to test IPython
5 itself from the command line. There are two ways of running this script:
5 itself from the command line. There are two ways of running this script:
6
6
7 1. With the syntax `iptest all`. This runs our entire test suite by
7 1. With the syntax `iptest all`. This runs our entire test suite by
8 calling this script (with different arguments) recursively. This
8 calling this script (with different arguments) recursively. This
9 causes modules and package to be tested in different processes, using nose
9 causes modules and package to be tested in different processes, using nose
10 or trial where appropriate.
10 or trial where appropriate.
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 the script simply calls nose, but with special command line flags and
12 the script simply calls nose, but with special command line flags and
13 plugins loaded.
13 plugins loaded.
14
14
15 """
15 """
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Copyright (C) 2009-2011 The IPython Development Team
18 # Copyright (C) 2009-2011 The IPython Development Team
19 #
19 #
20 # Distributed under the terms of the BSD License. The full license is in
20 # Distributed under the terms of the BSD License. The full license is in
21 # the file COPYING, distributed as part of this software.
21 # the file COPYING, distributed as part of this software.
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Imports
25 # Imports
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 from __future__ import print_function
27 from __future__ import print_function
28
28
29 # Stdlib
29 # Stdlib
30 import glob
30 import glob
31 import os
31 import os
32 import os.path as path
32 import os.path as path
33 import signal
33 import signal
34 import sys
34 import sys
35 import subprocess
35 import subprocess
36 import tempfile
36 import tempfile
37 import time
37 import time
38 import warnings
38 import warnings
39 import multiprocessing.pool
39 import multiprocessing.pool
40
40
41 # Now, proceed to import nose itself
41 # Now, proceed to import nose itself
42 import nose.plugins.builtin
42 import nose.plugins.builtin
43 from nose.plugins.xunit import Xunit
43 from nose.plugins.xunit import Xunit
44 from nose import SkipTest
44 from nose import SkipTest
45 from nose.core import TestProgram
45 from nose.core import TestProgram
46
46
47 # Our own imports
47 # Our own imports
48 from IPython.utils import py3compat
48 from IPython.utils import py3compat
49 from IPython.utils.importstring import import_item
49 from IPython.utils.importstring import import_item
50 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
50 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
51 from IPython.utils.process import pycmd2argv
51 from IPython.utils.process import pycmd2argv
52 from IPython.utils.sysinfo import sys_info
52 from IPython.utils.sysinfo import sys_info
53 from IPython.utils.tempdir import TemporaryDirectory
53 from IPython.utils.tempdir import TemporaryDirectory
54 from IPython.utils.warn import warn
54 from IPython.utils.warn import warn
55
55
56 from IPython.testing import globalipapp
56 from IPython.testing import globalipapp
57 from IPython.testing.plugin.ipdoctest import IPythonDoctest
57 from IPython.testing.plugin.ipdoctest import IPythonDoctest
58 from IPython.external.decorators import KnownFailure, knownfailureif
58 from IPython.external.decorators import KnownFailure, knownfailureif
59
59
60 pjoin = path.join
60 pjoin = path.join
61
61
62
62
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64 # Globals
64 # Globals
65 #-----------------------------------------------------------------------------
65 #-----------------------------------------------------------------------------
66
66
67
67
68 #-----------------------------------------------------------------------------
68 #-----------------------------------------------------------------------------
69 # Warnings control
69 # Warnings control
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71
71
72 # Twisted generates annoying warnings with Python 2.6, as will do other code
72 # Twisted generates annoying warnings with Python 2.6, as will do other code
73 # that imports 'sets' as of today
73 # that imports 'sets' as of today
74 warnings.filterwarnings('ignore', 'the sets module is deprecated',
74 warnings.filterwarnings('ignore', 'the sets module is deprecated',
75 DeprecationWarning )
75 DeprecationWarning )
76
76
77 # This one also comes from Twisted
77 # This one also comes from Twisted
78 warnings.filterwarnings('ignore', 'the sha module is deprecated',
78 warnings.filterwarnings('ignore', 'the sha module is deprecated',
79 DeprecationWarning)
79 DeprecationWarning)
80
80
81 # Wx on Fedora11 spits these out
81 # Wx on Fedora11 spits these out
82 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
82 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
83 UserWarning)
83 UserWarning)
84
84
85 # ------------------------------------------------------------------------------
85 # ------------------------------------------------------------------------------
86 # Monkeypatch Xunit to count known failures as skipped.
86 # Monkeypatch Xunit to count known failures as skipped.
87 # ------------------------------------------------------------------------------
87 # ------------------------------------------------------------------------------
88 def monkeypatch_xunit():
88 def monkeypatch_xunit():
89 try:
89 try:
90 knownfailureif(True)(lambda: None)()
90 knownfailureif(True)(lambda: None)()
91 except Exception as e:
91 except Exception as e:
92 KnownFailureTest = type(e)
92 KnownFailureTest = type(e)
93
93
94 def addError(self, test, err, capt=None):
94 def addError(self, test, err, capt=None):
95 if issubclass(err[0], KnownFailureTest):
95 if issubclass(err[0], KnownFailureTest):
96 err = (SkipTest,) + err[1:]
96 err = (SkipTest,) + err[1:]
97 return self.orig_addError(test, err, capt)
97 return self.orig_addError(test, err, capt)
98
98
99 Xunit.orig_addError = Xunit.addError
99 Xunit.orig_addError = Xunit.addError
100 Xunit.addError = addError
100 Xunit.addError = addError
101
101
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103 # Logic for skipping doctests
103 # Logic for skipping doctests
104 #-----------------------------------------------------------------------------
104 #-----------------------------------------------------------------------------
105 def extract_version(mod):
105 def extract_version(mod):
106 return mod.__version__
106 return mod.__version__
107
107
108 def test_for(item, min_version=None, callback=extract_version):
108 def test_for(item, min_version=None, callback=extract_version):
109 """Test to see if item is importable, and optionally check against a minimum
109 """Test to see if item is importable, and optionally check against a minimum
110 version.
110 version.
111
111
112 If min_version is given, the default behavior is to check against the
112 If min_version is given, the default behavior is to check against the
113 `__version__` attribute of the item, but specifying `callback` allows you to
113 `__version__` attribute of the item, but specifying `callback` allows you to
114 extract the value you are interested in. e.g::
114 extract the value you are interested in. e.g::
115
115
116 In [1]: import sys
116 In [1]: import sys
117
117
118 In [2]: from IPython.testing.iptest import test_for
118 In [2]: from IPython.testing.iptest import test_for
119
119
120 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
120 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
121 Out[3]: True
121 Out[3]: True
122
122
123 """
123 """
124 try:
124 try:
125 check = import_item(item)
125 check = import_item(item)
126 except (ImportError, RuntimeError):
126 except (ImportError, RuntimeError):
127 # GTK reports Runtime error if it can't be initialized even if it's
127 # GTK reports Runtime error if it can't be initialized even if it's
128 # importable.
128 # importable.
129 return False
129 return False
130 else:
130 else:
131 if min_version:
131 if min_version:
132 if callback:
132 if callback:
133 # extra processing step to get version to compare
133 # extra processing step to get version to compare
134 check = callback(check)
134 check = callback(check)
135
135
136 return check >= min_version
136 return check >= min_version
137 else:
137 else:
138 return True
138 return True
139
139
140 # Global dict where we can store information on what we have and what we don't
140 # Global dict where we can store information on what we have and what we don't
141 # have available at test run time
141 # have available at test run time
142 have = {}
142 have = {}
143
143
144 have['curses'] = test_for('_curses')
144 have['curses'] = test_for('_curses')
145 have['matplotlib'] = test_for('matplotlib')
145 have['matplotlib'] = test_for('matplotlib')
146 have['numpy'] = test_for('numpy')
146 have['numpy'] = test_for('numpy')
147 have['pexpect'] = test_for('IPython.external.pexpect')
147 have['pexpect'] = test_for('IPython.external.pexpect')
148 have['pymongo'] = test_for('pymongo')
148 have['pymongo'] = test_for('pymongo')
149 have['pygments'] = test_for('pygments')
149 have['pygments'] = test_for('pygments')
150 have['qt'] = test_for('IPython.external.qt')
150 have['qt'] = test_for('IPython.external.qt')
151 have['rpy2'] = test_for('rpy2')
151 have['rpy2'] = test_for('rpy2')
152 have['sqlite3'] = test_for('sqlite3')
152 have['sqlite3'] = test_for('sqlite3')
153 have['cython'] = test_for('Cython')
153 have['cython'] = test_for('Cython')
154 have['oct2py'] = test_for('oct2py')
154 have['oct2py'] = test_for('oct2py')
155 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
155 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
156 have['jinja2'] = test_for('jinja2')
156 have['jinja2'] = test_for('jinja2')
157 have['wx'] = test_for('wx')
157 have['wx'] = test_for('wx')
158 have['wx.aui'] = test_for('wx.aui')
158 have['wx.aui'] = test_for('wx.aui')
159 have['azure'] = test_for('azure')
159 have['azure'] = test_for('azure')
160 have['sphinx'] = test_for('sphinx')
160 have['sphinx'] = test_for('sphinx')
161
161
162 min_zmq = (2,1,11)
162 min_zmq = (2,1,11)
163
163
164 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
164 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
165
165
166 #-----------------------------------------------------------------------------
166 #-----------------------------------------------------------------------------
167 # Functions and classes
167 # Functions and classes
168 #-----------------------------------------------------------------------------
168 #-----------------------------------------------------------------------------
169
169
170 def report():
170 def report():
171 """Return a string with a summary report of test-related variables."""
171 """Return a string with a summary report of test-related variables."""
172
172
173 out = [ sys_info(), '\n']
173 out = [ sys_info(), '\n']
174
174
175 avail = []
175 avail = []
176 not_avail = []
176 not_avail = []
177
177
178 for k, is_avail in have.items():
178 for k, is_avail in have.items():
179 if is_avail:
179 if is_avail:
180 avail.append(k)
180 avail.append(k)
181 else:
181 else:
182 not_avail.append(k)
182 not_avail.append(k)
183
183
184 if avail:
184 if avail:
185 out.append('\nTools and libraries available at test time:\n')
185 out.append('\nTools and libraries available at test time:\n')
186 avail.sort()
186 avail.sort()
187 out.append(' ' + ' '.join(avail)+'\n')
187 out.append(' ' + ' '.join(avail)+'\n')
188
188
189 if not_avail:
189 if not_avail:
190 out.append('\nTools and libraries NOT available at test time:\n')
190 out.append('\nTools and libraries NOT available at test time:\n')
191 not_avail.sort()
191 not_avail.sort()
192 out.append(' ' + ' '.join(not_avail)+'\n')
192 out.append(' ' + ' '.join(not_avail)+'\n')
193
193
194 return ''.join(out)
194 return ''.join(out)
195
195
196
196
197 def make_exclude():
197 def make_exclude():
198 """Make patterns of modules and packages to exclude from testing.
198 """Make patterns of modules and packages to exclude from testing.
199
199
200 For the IPythonDoctest plugin, we need to exclude certain patterns that
200 For the IPythonDoctest plugin, we need to exclude certain patterns that
201 cause testing problems. We should strive to minimize the number of
201 cause testing problems. We should strive to minimize the number of
202 skipped modules, since this means untested code.
202 skipped modules, since this means untested code.
203
203
204 These modules and packages will NOT get scanned by nose at all for tests.
204 These modules and packages will NOT get scanned by nose at all for tests.
205 """
205 """
206 # Simple utility to make IPython paths more readably, we need a lot of
206 # Simple utility to make IPython paths more readably, we need a lot of
207 # these below
207 # these below
208 ipjoin = lambda *paths: pjoin('IPython', *paths)
208 ipjoin = lambda *paths: pjoin('IPython', *paths)
209
209
210 exclusions = [ipjoin('external'),
210 exclusions = [ipjoin('external'),
211 ipjoin('quarantine'),
211 ipjoin('quarantine'),
212 ipjoin('deathrow'),
212 ipjoin('deathrow'),
213 # This guy is probably attic material
213 # This guy is probably attic material
214 ipjoin('testing', 'mkdoctests'),
214 ipjoin('testing', 'mkdoctests'),
215 # Testing inputhook will need a lot of thought, to figure out
215 # Testing inputhook will need a lot of thought, to figure out
216 # how to have tests that don't lock up with the gui event
216 # how to have tests that don't lock up with the gui event
217 # loops in the picture
217 # loops in the picture
218 ipjoin('lib', 'inputhook'),
218 ipjoin('lib', 'inputhook'),
219 # Config files aren't really importable stand-alone
219 # Config files aren't really importable stand-alone
220 ipjoin('config', 'profile'),
220 ipjoin('config', 'profile'),
221 # The notebook 'static' directory contains JS, css and other
221 # The notebook 'static' directory contains JS, css and other
222 # files for web serving. Occasionally projects may put a .py
222 # files for web serving. Occasionally projects may put a .py
223 # file in there (MathJax ships a conf.py), so we might as
223 # file in there (MathJax ships a conf.py), so we might as
224 # well play it safe and skip the whole thing.
224 # well play it safe and skip the whole thing.
225 ipjoin('html', 'static'),
225 ipjoin('html', 'static'),
226 ipjoin('html', 'fabfile'),
226 ipjoin('html', 'fabfile'),
227 ]
227 ]
228 if not have['sqlite3']:
228 if not have['sqlite3']:
229 exclusions.append(ipjoin('core', 'tests', 'test_history'))
229 exclusions.append(ipjoin('core', 'tests', 'test_history'))
230 exclusions.append(ipjoin('core', 'history'))
230 exclusions.append(ipjoin('core', 'history'))
231 if not have['wx']:
231 if not have['wx']:
232 exclusions.append(ipjoin('lib', 'inputhookwx'))
232 exclusions.append(ipjoin('lib', 'inputhookwx'))
233
233
234 if 'IPython.kernel.inprocess' not in sys.argv:
234 if 'IPython.kernel.inprocess' not in sys.argv:
235 exclusions.append(ipjoin('kernel', 'inprocess'))
235 exclusions.append(ipjoin('kernel', 'inprocess'))
236
236
237 # FIXME: temporarily disable autoreload tests, as they can produce
237 # FIXME: temporarily disable autoreload tests, as they can produce
238 # spurious failures in subsequent tests (cythonmagic).
238 # spurious failures in subsequent tests (cythonmagic).
239 exclusions.append(ipjoin('extensions', 'autoreload'))
239 exclusions.append(ipjoin('extensions', 'autoreload'))
240 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
240 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
241
241
242 # We do this unconditionally, so that the test suite doesn't import
242 # We do this unconditionally, so that the test suite doesn't import
243 # gtk, changing the default encoding and masking some unicode bugs.
243 # gtk, changing the default encoding and masking some unicode bugs.
244 exclusions.append(ipjoin('lib', 'inputhookgtk'))
244 exclusions.append(ipjoin('lib', 'inputhookgtk'))
245 exclusions.append(ipjoin('kernel', 'zmq', 'gui', 'gtkembed'))
245 exclusions.append(ipjoin('kernel', 'zmq', 'gui', 'gtkembed'))
246
246
247 #Also done unconditionally, exclude nbconvert directories containing
247 #Also done unconditionally, exclude nbconvert directories containing
248 #config files used to test. Executing the config files with iptest would
248 #config files used to test. Executing the config files with iptest would
249 #cause an exception.
249 #cause an exception.
250 exclusions.append(ipjoin('nbconvert', 'tests', 'files'))
250 exclusions.append(ipjoin('nbconvert', 'tests', 'files'))
251 exclusions.append(ipjoin('nbconvert', 'exporters', 'tests', 'files'))
251 exclusions.append(ipjoin('nbconvert', 'exporters', 'tests', 'files'))
252
252
253 # These have to be skipped on win32 because the use echo, rm, cd, etc.
253 # These have to be skipped on win32 because the use echo, rm, cd, etc.
254 # See ticket https://github.com/ipython/ipython/issues/87
254 # See ticket https://github.com/ipython/ipython/issues/87
255 if sys.platform == 'win32':
255 if sys.platform == 'win32':
256 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
256 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
257 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
257 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
258
258
259 if not have['pexpect']:
259 if not have['pexpect']:
260 exclusions.extend([ipjoin('lib', 'irunner'),
260 exclusions.extend([ipjoin('lib', 'irunner'),
261 ipjoin('lib', 'tests', 'test_irunner'),
261 ipjoin('lib', 'tests', 'test_irunner'),
262 ipjoin('terminal', 'console'),
262 ipjoin('terminal', 'console'),
263 ])
263 ])
264
264
265 if not have['zmq']:
265 if not have['zmq']:
266 exclusions.append(ipjoin('lib', 'kernel'))
266 exclusions.append(ipjoin('lib', 'kernel'))
267 exclusions.append(ipjoin('kernel'))
267 exclusions.append(ipjoin('kernel'))
268 exclusions.append(ipjoin('qt'))
268 exclusions.append(ipjoin('qt'))
269 exclusions.append(ipjoin('html'))
269 exclusions.append(ipjoin('html'))
270 exclusions.append(ipjoin('consoleapp.py'))
270 exclusions.append(ipjoin('consoleapp.py'))
271 exclusions.append(ipjoin('terminal', 'console'))
271 exclusions.append(ipjoin('terminal', 'console'))
272 exclusions.append(ipjoin('parallel'))
272 exclusions.append(ipjoin('parallel'))
273 elif not have['qt'] or not have['pygments']:
273 elif not have['qt'] or not have['pygments']:
274 exclusions.append(ipjoin('qt'))
274 exclusions.append(ipjoin('qt'))
275
275
276 if not have['pymongo']:
276 if not have['pymongo']:
277 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
277 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
278 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
278 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
279
279
280 if not have['matplotlib']:
280 if not have['matplotlib']:
281 exclusions.extend([ipjoin('core', 'pylabtools'),
281 exclusions.extend([ipjoin('core', 'pylabtools'),
282 ipjoin('core', 'tests', 'test_pylabtools'),
282 ipjoin('core', 'tests', 'test_pylabtools'),
283 ipjoin('kernel', 'zmq', 'pylab'),
283 ipjoin('kernel', 'zmq', 'pylab'),
284 ])
284 ])
285
285
286 if not have['cython']:
286 if not have['cython']:
287 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
287 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
288 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
288 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
289
289
290 if not have['oct2py']:
290 if not have['oct2py']:
291 exclusions.extend([ipjoin('extensions', 'octavemagic')])
291 exclusions.extend([ipjoin('extensions', 'octavemagic')])
292 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
292 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
293
293
294 if not have['tornado']:
294 if not have['tornado']:
295 exclusions.append(ipjoin('html'))
295 exclusions.append(ipjoin('html'))
296
296
297 if not have['jinja2']:
297 if not have['jinja2']:
298 exclusions.append(ipjoin('html', 'notebookapp'))
298 exclusions.append(ipjoin('html', 'notebookapp'))
299
299
300 if not have['rpy2'] or not have['numpy']:
300 if not have['rpy2'] or not have['numpy']:
301 exclusions.append(ipjoin('extensions', 'rmagic'))
301 exclusions.append(ipjoin('extensions', 'rmagic'))
302 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
302 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
303
303
304 if not have['azure']:
304 if not have['azure']:
305 exclusions.append(ipjoin('html', 'services', 'notebooks', 'azurenbmanager'))
305 exclusions.append(ipjoin('html', 'services', 'notebooks', 'azurenbmanager'))
306
306
307 if not all((have['pygments'], have['jinja2'], have['sphinx'])):
307 if not all((have['pygments'], have['jinja2'], have['sphinx'])):
308 exclusions.append(ipjoin('nbconvert'))
308 exclusions.append(ipjoin('nbconvert'))
309
309
310 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
310 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
311 if sys.platform == 'win32':
311 if sys.platform == 'win32':
312 exclusions = [s.replace('\\','\\\\') for s in exclusions]
312 exclusions = [s.replace('\\','\\\\') for s in exclusions]
313
313
314 # check for any exclusions that don't seem to exist:
314 # check for any exclusions that don't seem to exist:
315 parent, _ = os.path.split(get_ipython_package_dir())
315 parent, _ = os.path.split(get_ipython_package_dir())
316 for exclusion in exclusions:
316 for exclusion in exclusions:
317 if exclusion.endswith(('deathrow', 'quarantine')):
317 if exclusion.endswith(('deathrow', 'quarantine')):
318 # ignore deathrow/quarantine, which exist in dev, but not install
318 # ignore deathrow/quarantine, which exist in dev, but not install
319 continue
319 continue
320 fullpath = pjoin(parent, exclusion)
320 fullpath = pjoin(parent, exclusion)
321 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
321 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
322 warn("Excluding nonexistent file: %r" % exclusion)
322 warn("Excluding nonexistent file: %r" % exclusion)
323
323
324 return exclusions
324 return exclusions
325
325
326
326
327 class IPTester(object):
327 class IPTester(object):
328 """Call that calls iptest or trial in a subprocess.
328 """Call that calls iptest or trial in a subprocess.
329 """
329 """
330 #: string, name of test runner that will be called
330 #: string, name of test runner that will be called
331 runner = None
331 runner = None
332 #: list, parameters for test runner
332 #: list, parameters for test runner
333 params = None
333 params = None
334 #: list, arguments of system call to be made to call test runner
334 #: list, arguments of system call to be made to call test runner
335 call_args = None
335 call_args = None
336 #: list, subprocesses we start (for cleanup)
336 #: list, subprocesses we start (for cleanup)
337 processes = None
337 processes = None
338 #: str, coverage xml output file
338 #: str, coverage xml output file
339 coverage_xml = None
339 coverage_xml = None
340 buffer_output = False
340 buffer_output = False
341
341
342 def __init__(self, runner='iptest', params=None):
342 def __init__(self, runner='iptest', params=None):
343 """Create new test runner."""
343 """Create new test runner."""
344 p = os.path
344 p = os.path
345 if runner == 'iptest':
345 if runner == 'iptest':
346 iptest_app = os.path.abspath(get_ipython_module_path('IPython.testing.iptest'))
346 iptest_app = os.path.abspath(get_ipython_module_path('IPython.testing.iptest'))
347 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
347 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
348 else:
348 else:
349 raise Exception('Not a valid test runner: %s' % repr(runner))
349 raise Exception('Not a valid test runner: %s' % repr(runner))
350 if params is None:
350 if params is None:
351 params = []
351 params = []
352 if isinstance(params, str):
352 if isinstance(params, str):
353 params = [params]
353 params = [params]
354 self.params = params
354 self.params = params
355
355
356 # Assemble call
356 # Assemble call
357 self.call_args = self.runner+self.params
357 self.call_args = self.runner+self.params
358
358
359 # Find the section we're testing (IPython.foo)
359 # Find the section we're testing (IPython.foo)
360 for sect in self.params:
360 for sect in self.params:
361 if sect.startswith('IPython') or sect in special_test_suites: break
361 if sect.startswith('IPython') or sect in special_test_suites: break
362 else:
362 else:
363 raise ValueError("Section not found", self.params)
363 raise ValueError("Section not found", self.params)
364
364
365 if '--with-xunit' in self.call_args:
365 if '--with-xunit' in self.call_args:
366
366
367 self.call_args.append('--xunit-file')
367 self.call_args.append('--xunit-file')
368 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
368 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
369 xunit_file = path.abspath(sect+'.xunit.xml')
369 xunit_file = path.abspath(sect+'.xunit.xml')
370 if sys.platform == 'win32':
370 if sys.platform == 'win32':
371 xunit_file = '"%s"' % xunit_file
371 xunit_file = '"%s"' % xunit_file
372 self.call_args.append(xunit_file)
372 self.call_args.append(xunit_file)
373
373
374 if '--with-xml-coverage' in self.call_args:
374 if '--with-xml-coverage' in self.call_args:
375 self.coverage_xml = path.abspath(sect+".coverage.xml")
375 self.coverage_xml = path.abspath(sect+".coverage.xml")
376 self.call_args.remove('--with-xml-coverage')
376 self.call_args.remove('--with-xml-coverage')
377 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
377 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
378
378
379 # Store anything we start to clean up on deletion
379 # Store anything we start to clean up on deletion
380 self.processes = []
380 self.processes = []
381
381
382 def _run_cmd(self):
382 def _run_cmd(self):
383 with TemporaryDirectory() as IPYTHONDIR:
383 with TemporaryDirectory() as IPYTHONDIR:
384 env = os.environ.copy()
384 env = os.environ.copy()
385 env['IPYTHONDIR'] = IPYTHONDIR
385 env['IPYTHONDIR'] = IPYTHONDIR
386 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
386 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
387 output = subprocess.PIPE if self.buffer_output else None
387 output = subprocess.PIPE if self.buffer_output else None
388 subp = subprocess.Popen(self.call_args, stdout=output,
388 subp = subprocess.Popen(self.call_args, stdout=output,
389 stderr=output, env=env)
389 stderr=output, env=env)
390 self.processes.append(subp)
390 self.processes.append(subp)
391 # If this fails, the process will be left in self.processes and
391 # If this fails, the process will be left in self.processes and
392 # cleaned up later, but if the wait call succeeds, then we can
392 # cleaned up later, but if the wait call succeeds, then we can
393 # clear the stored process.
393 # clear the stored process.
394 retcode = subp.wait()
394 retcode = subp.wait()
395 self.processes.pop()
395 self.processes.pop()
396 self.stdout = subp.stdout
396 self.stdout = subp.stdout
397 self.stderr = subp.stderr
397 self.stderr = subp.stderr
398 return retcode
398 return retcode
399
399
400 def run(self):
400 def run(self):
401 """Run the stored commands"""
401 """Run the stored commands"""
402 try:
402 try:
403 retcode = self._run_cmd()
403 retcode = self._run_cmd()
404 #print(self.stdout.read())
404 #print(self.stdout.read())
405 #print("std err")
405 #print("std err")
406 #print(self.stderr.read())
406 #print(self.stderr.read())
407 except KeyboardInterrupt:
407 except KeyboardInterrupt:
408 return -signal.SIGINT
408 return -signal.SIGINT
409 except:
409 except:
410 import traceback
410 import traceback
411 traceback.print_exc()
411 traceback.print_exc()
412 return 1 # signal failure
412 return 1 # signal failure
413
413
414 if self.coverage_xml:
414 if self.coverage_xml:
415 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
415 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
416 return retcode
416 return retcode
417
417
418 def __del__(self):
418 def __del__(self):
419 """Cleanup on exit by killing any leftover processes."""
419 """Cleanup on exit by killing any leftover processes."""
420 for subp in self.processes:
420 for subp in self.processes:
421 if subp.poll() is not None:
421 if subp.poll() is not None:
422 continue # process is already dead
422 continue # process is already dead
423
423
424 try:
424 try:
425 print('Cleaning up stale PID: %d' % subp.pid)
425 print('Cleaning up stale PID: %d' % subp.pid)
426 subp.kill()
426 subp.kill()
427 except: # (OSError, WindowsError) ?
427 except: # (OSError, WindowsError) ?
428 # This is just a best effort, if we fail or the process was
428 # This is just a best effort, if we fail or the process was
429 # really gone, ignore it.
429 # really gone, ignore it.
430 pass
430 pass
431 else:
431 else:
432 for i in range(10):
432 for i in range(10):
433 if subp.poll() is None:
433 if subp.poll() is None:
434 time.sleep(0.1)
434 time.sleep(0.1)
435 else:
435 else:
436 break
436 break
437
437
438 if subp.poll() is None:
438 if subp.poll() is None:
439 # The process did not die...
439 # The process did not die...
440 print('... failed. Manual cleanup may be required.')
440 print('... failed. Manual cleanup may be required.')
441
441
442
442
443 special_test_suites = {
443 special_test_suites = {
444 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
444 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
445 }
445 }
446
446
447 def make_runners(inc_slow=False):
447 def make_runners(inc_slow=False):
448 """Define the top-level packages that need to be tested.
448 """Define the top-level packages that need to be tested.
449 """
449 """
450
450
451 # Packages to be tested via nose, that only depend on the stdlib
451 # Packages to be tested via nose, that only depend on the stdlib
452 nose_pkg_names = ['config', 'core', 'extensions', 'lib', 'terminal',
452 nose_pkg_names = ['config', 'core', 'extensions', 'lib', 'terminal',
453 'testing', 'utils', 'nbformat']
453 'testing', 'utils', 'nbformat']
454
454
455 if have['qt']:
455 if have['qt']:
456 nose_pkg_names.append('qt')
456 nose_pkg_names.append('qt')
457
457
458 if have['tornado']:
458 if have['tornado']:
459 nose_pkg_names.append('html')
459 nose_pkg_names.append('html')
460
460
461 if have['zmq']:
461 if have['zmq']:
462 nose_pkg_names.insert(0, 'kernel')
462 nose_pkg_names.insert(0, 'kernel')
463 nose_pkg_names.insert(1, 'kernel.inprocess')
463 nose_pkg_names.insert(1, 'kernel.inprocess')
464 if inc_slow:
464 if inc_slow:
465 nose_pkg_names.append('parallel')
465 nose_pkg_names.insert(0, 'parallel')
466
466
467 if all((have['pygments'], have['jinja2'], have['sphinx'])):
467 if all((have['pygments'], have['jinja2'], have['sphinx'])):
468 nose_pkg_names.append('nbconvert')
468 nose_pkg_names.append('nbconvert')
469
469
470 # For debugging this code, only load quick stuff
470 # For debugging this code, only load quick stuff
471 #nose_pkg_names = ['core', 'extensions'] # dbg
471 #nose_pkg_names = ['core', 'extensions'] # dbg
472
472
473 # Make fully qualified package names prepending 'IPython.' to our name lists
473 # Make fully qualified package names prepending 'IPython.' to our name lists
474 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
474 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
475
475
476 # Make runners
476 # Make runners
477 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
477 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
478
478
479 for name in special_test_suites:
479 for name in special_test_suites:
480 runners.append((name, IPTester('iptest', params=name)))
480 runners.append((name, IPTester('iptest', params=name)))
481
481
482 return runners
482 return runners
483
483
484
484
485 def run_iptest():
485 def run_iptest():
486 """Run the IPython test suite using nose.
486 """Run the IPython test suite using nose.
487
487
488 This function is called when this script is **not** called with the form
488 This function is called when this script is **not** called with the form
489 `iptest all`. It simply calls nose with appropriate command line flags
489 `iptest all`. It simply calls nose with appropriate command line flags
490 and accepts all of the standard nose arguments.
490 and accepts all of the standard nose arguments.
491 """
491 """
492 # Apply our monkeypatch to Xunit
492 # Apply our monkeypatch to Xunit
493 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
493 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
494 monkeypatch_xunit()
494 monkeypatch_xunit()
495
495
496 warnings.filterwarnings('ignore',
496 warnings.filterwarnings('ignore',
497 'This will be removed soon. Use IPython.testing.util instead')
497 'This will be removed soon. Use IPython.testing.util instead')
498
498
499 if sys.argv[1] in special_test_suites:
499 if sys.argv[1] in special_test_suites:
500 sys.argv[1:2] = special_test_suites[sys.argv[1]]
500 sys.argv[1:2] = special_test_suites[sys.argv[1]]
501 special_suite = True
501 special_suite = True
502 else:
502 else:
503 special_suite = False
503 special_suite = False
504
504
505 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
505 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
506
506
507 '--with-ipdoctest',
507 '--with-ipdoctest',
508 '--ipdoctest-tests','--ipdoctest-extension=txt',
508 '--ipdoctest-tests','--ipdoctest-extension=txt',
509
509
510 # We add --exe because of setuptools' imbecility (it
510 # We add --exe because of setuptools' imbecility (it
511 # blindly does chmod +x on ALL files). Nose does the
511 # blindly does chmod +x on ALL files). Nose does the
512 # right thing and it tries to avoid executables,
512 # right thing and it tries to avoid executables,
513 # setuptools unfortunately forces our hand here. This
513 # setuptools unfortunately forces our hand here. This
514 # has been discussed on the distutils list and the
514 # has been discussed on the distutils list and the
515 # setuptools devs refuse to fix this problem!
515 # setuptools devs refuse to fix this problem!
516 '--exe',
516 '--exe',
517 ]
517 ]
518 if '-a' not in argv and '-A' not in argv:
518 if '-a' not in argv and '-A' not in argv:
519 argv = argv + ['-a', '!crash']
519 argv = argv + ['-a', '!crash']
520
520
521 if nose.__version__ >= '0.11':
521 if nose.__version__ >= '0.11':
522 # I don't fully understand why we need this one, but depending on what
522 # I don't fully understand why we need this one, but depending on what
523 # directory the test suite is run from, if we don't give it, 0 tests
523 # directory the test suite is run from, if we don't give it, 0 tests
524 # get run. Specifically, if the test suite is run from the source dir
524 # get run. Specifically, if the test suite is run from the source dir
525 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
525 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
526 # even if the same call done in this directory works fine). It appears
526 # even if the same call done in this directory works fine). It appears
527 # that if the requested package is in the current dir, nose bails early
527 # that if the requested package is in the current dir, nose bails early
528 # by default. Since it's otherwise harmless, leave it in by default
528 # by default. Since it's otherwise harmless, leave it in by default
529 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
529 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
530 argv.append('--traverse-namespace')
530 argv.append('--traverse-namespace')
531
531
532 # use our plugin for doctesting. It will remove the standard doctest plugin
532 # use our plugin for doctesting. It will remove the standard doctest plugin
533 # if it finds it enabled
533 # if it finds it enabled
534 ipdt = IPythonDoctest() if special_suite else IPythonDoctest(make_exclude())
534 ipdt = IPythonDoctest() if special_suite else IPythonDoctest(make_exclude())
535 plugins = [ipdt, KnownFailure()]
535 plugins = [ipdt, KnownFailure()]
536
536
537 # We need a global ipython running in this process, but the special
537 # We need a global ipython running in this process, but the special
538 # in-process group spawns its own IPython kernels, so for *that* group we
538 # in-process group spawns its own IPython kernels, so for *that* group we
539 # must avoid also opening the global one (otherwise there's a conflict of
539 # must avoid also opening the global one (otherwise there's a conflict of
540 # singletons). Ultimately the solution to this problem is to refactor our
540 # singletons). Ultimately the solution to this problem is to refactor our
541 # assumptions about what needs to be a singleton and what doesn't (app
541 # assumptions about what needs to be a singleton and what doesn't (app
542 # objects should, individual shells shouldn't). But for now, this
542 # objects should, individual shells shouldn't). But for now, this
543 # workaround allows the test suite for the inprocess module to complete.
543 # workaround allows the test suite for the inprocess module to complete.
544 if not 'IPython.kernel.inprocess' in sys.argv:
544 if not 'IPython.kernel.inprocess' in sys.argv:
545 globalipapp.start_ipython()
545 globalipapp.start_ipython()
546
546
547 # Now nose can run
547 # Now nose can run
548 TestProgram(argv=argv, addplugins=plugins)
548 TestProgram(argv=argv, addplugins=plugins)
549
549
550 def do_run(x):
550 def do_run(x):
551 print('IPython test group:',x[0])
551 print('IPython test group:',x[0])
552 ret = x[1].run()
552 ret = x[1].run()
553 return ret
553 return ret
554
554
555 def run_iptestall(inc_slow=False, fast=False):
555 def run_iptestall(inc_slow=False, fast=False):
556 """Run the entire IPython test suite by calling nose and trial.
556 """Run the entire IPython test suite by calling nose and trial.
557
557
558 This function constructs :class:`IPTester` instances for all IPython
558 This function constructs :class:`IPTester` instances for all IPython
559 modules and package and then runs each of them. This causes the modules
559 modules and package and then runs each of them. This causes the modules
560 and packages of IPython to be tested each in their own subprocess using
560 and packages of IPython to be tested each in their own subprocess using
561 nose.
561 nose.
562
562
563 Parameters
563 Parameters
564 ----------
564 ----------
565
565
566 inc_slow : bool, optional
566 inc_slow : bool, optional
567 Include slow tests, like IPython.parallel. By default, these tests aren't
567 Include slow tests, like IPython.parallel. By default, these tests aren't
568 run.
568 run.
569
569
570 fast : bool, option
570 fast : bool, option
571 Run the test suite in parallel, if True, using as many threads as there
571 Run the test suite in parallel, if True, using as many threads as there
572 are processors
572 are processors
573 """
573 """
574 if fast:
574 if fast:
575 p = multiprocessing.pool.ThreadPool()
575 p = multiprocessing.pool.ThreadPool()
576 else:
576 else:
577 p = multiprocessing.pool.ThreadPool(1)
577 p = multiprocessing.pool.ThreadPool(1)
578
578
579 runners = make_runners(inc_slow=inc_slow)
579 runners = make_runners(inc_slow=inc_slow)
580
580
581 # Run the test runners in a temporary dir so we can nuke it when finished
581 # Run the test runners in a temporary dir so we can nuke it when finished
582 # to clean up any junk files left over by accident. This also makes it
582 # to clean up any junk files left over by accident. This also makes it
583 # robust against being run in non-writeable directories by mistake, as the
583 # robust against being run in non-writeable directories by mistake, as the
584 # temp dir will always be user-writeable.
584 # temp dir will always be user-writeable.
585 curdir = os.getcwdu()
585 curdir = os.getcwdu()
586 testdir = tempfile.gettempdir()
586 testdir = tempfile.gettempdir()
587 os.chdir(testdir)
587 os.chdir(testdir)
588
588
589 # Run all test runners, tracking execution time
589 # Run all test runners, tracking execution time
590 failed = []
590 failed = []
591 t_start = time.time()
591 t_start = time.time()
592
592
593 #runners = runners[::-1]
593 #runners = runners[::-1]
594
594
595 print([r[0] for r in runners])
595 print([r[0] for r in runners])
596
596
597 try:
597 try:
598
598
599 print(len(runners))
599 print(len(runners))
600 all_res = p.map(do_run, runners)
600 all_res = p.map(do_run, runners)
601 print('*'*70)
601 print('*'*70)
602 for ((name, runner), res) in zip(runners, all_res):
602 for ((name, runner), res) in zip(runners, all_res):
603 print(' '*70)
603 print(' '*70)
604 tgroup = 'IPython test group: ' + name
604 tgroup = 'IPython test group: ' + name
605 res_string = 'OK' if res == 0 else 'FAILED'
605 res_string = 'OK' if res == 0 else 'FAILED'
606 res_string = res_string.rjust(70 - len(tgroup), '.')
606 res_string = res_string.rjust(70 - len(tgroup), '.')
607 print(tgroup + res_string)
607 print(tgroup + res_string)
608 if res:
608 if res:
609 failed.append( (name, runner) )
609 failed.append( (name, runner) )
610 if res == -signal.SIGINT:
610 if res == -signal.SIGINT:
611 print("Interrupted")
611 print("Interrupted")
612 break
612 break
613 finally:
613 finally:
614 os.chdir(curdir)
614 os.chdir(curdir)
615 t_end = time.time()
615 t_end = time.time()
616 t_tests = t_end - t_start
616 t_tests = t_end - t_start
617 nrunners = len(runners)
617 nrunners = len(runners)
618 nfail = len(failed)
618 nfail = len(failed)
619 # summarize results
619 # summarize results
620 print()
620 print()
621 print('*'*70)
621 print('*'*70)
622 print('Test suite completed for system with the following information:')
622 print('Test suite completed for system with the following information:')
623 print(report())
623 print(report())
624 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
624 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
625 print()
625 print()
626 print('Status:')
626 print('Status:')
627 if not failed:
627 if not failed:
628 print('OK')
628 print('OK')
629 else:
629 else:
630 # If anything went wrong, point out what command to rerun manually to
630 # If anything went wrong, point out what command to rerun manually to
631 # see the actual errors and individual summary
631 # see the actual errors and individual summary
632 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
632 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
633 for name, failed_runner in failed:
633 for name, failed_runner in failed:
634 print('-'*40)
634 print('-'*40)
635 print('Runner failed:',name)
635 print('Runner failed:',name)
636 print('You may wish to rerun this one individually, with:')
636 print('You may wish to rerun this one individually, with:')
637 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
637 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
638 print(u' '.join(failed_call_args))
638 print(u' '.join(failed_call_args))
639 print()
639 print()
640 # Ensure that our exit code indicates failure
640 # Ensure that our exit code indicates failure
641 sys.exit(1)
641 sys.exit(1)
642
642
643
643
644 def main():
644 def main():
645 for arg in sys.argv[1:]:
645 for arg in sys.argv[1:]:
646 if arg.startswith('IPython') or arg in special_test_suites:
646 if arg.startswith('IPython') or arg in special_test_suites:
647 # This is in-process
647 # This is in-process
648 run_iptest()
648 run_iptest()
649 else:
649 else:
650 inc_slow = "--all" in sys.argv
650 inc_slow = "--all" in sys.argv
651 if inc_slow:
651 if inc_slow:
652 sys.argv.remove("--all")
652 sys.argv.remove("--all")
653
653
654 fast = "--fast" in sys.argv
654 fast = "--fast" in sys.argv
655 if fast:
655 if fast:
656 sys.argv.remove("--fast")
656 sys.argv.remove("--fast")
657 IPTester.buffer_output = True
657 IPTester.buffer_output = True
658
658
659 # This starts subprocesses
659 # This starts subprocesses
660 run_iptestall(inc_slow=inc_slow, fast=fast)
660 run_iptestall(inc_slow=inc_slow, fast=fast)
661
661
662
662
663 if __name__ == '__main__':
663 if __name__ == '__main__':
664 main()
664 main()
General Comments 0
You need to be logged in to leave comments. Login now