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