##// END OF EJS Templates
Allow drilling down to individual tests using iptest command
Thomas Kluyver -
Show More
@@ -1,425 +1,430 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 re
33 import re
34 import sys
34 import sys
35 import warnings
35 import warnings
36
36
37 # Now, proceed to import nose itself
37 # Now, proceed to import nose itself
38 import nose.plugins.builtin
38 import nose.plugins.builtin
39 from nose.plugins.xunit import Xunit
39 from nose.plugins.xunit import Xunit
40 from nose import SkipTest
40 from nose import SkipTest
41 from nose.core import TestProgram
41 from nose.core import TestProgram
42 from nose.plugins import Plugin
42 from nose.plugins import Plugin
43
43
44 # Our own imports
44 # Our own imports
45 from IPython.utils.importstring import import_item
45 from IPython.utils.importstring import import_item
46 from IPython.testing.plugin.ipdoctest import IPythonDoctest
46 from IPython.testing.plugin.ipdoctest import IPythonDoctest
47 from IPython.external.decorators import KnownFailure, knownfailureif
47 from IPython.external.decorators import KnownFailure, knownfailureif
48
48
49 pjoin = path.join
49 pjoin = path.join
50
50
51
51
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53 # Globals
53 # Globals
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55
55
56
56
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58 # Warnings control
58 # Warnings control
59 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
60
60
61 # Twisted generates annoying warnings with Python 2.6, as will do other code
61 # Twisted generates annoying warnings with Python 2.6, as will do other code
62 # that imports 'sets' as of today
62 # that imports 'sets' as of today
63 warnings.filterwarnings('ignore', 'the sets module is deprecated',
63 warnings.filterwarnings('ignore', 'the sets module is deprecated',
64 DeprecationWarning )
64 DeprecationWarning )
65
65
66 # This one also comes from Twisted
66 # This one also comes from Twisted
67 warnings.filterwarnings('ignore', 'the sha module is deprecated',
67 warnings.filterwarnings('ignore', 'the sha module is deprecated',
68 DeprecationWarning)
68 DeprecationWarning)
69
69
70 # Wx on Fedora11 spits these out
70 # Wx on Fedora11 spits these out
71 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
71 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
72 UserWarning)
72 UserWarning)
73
73
74 # ------------------------------------------------------------------------------
74 # ------------------------------------------------------------------------------
75 # Monkeypatch Xunit to count known failures as skipped.
75 # Monkeypatch Xunit to count known failures as skipped.
76 # ------------------------------------------------------------------------------
76 # ------------------------------------------------------------------------------
77 def monkeypatch_xunit():
77 def monkeypatch_xunit():
78 try:
78 try:
79 knownfailureif(True)(lambda: None)()
79 knownfailureif(True)(lambda: None)()
80 except Exception as e:
80 except Exception as e:
81 KnownFailureTest = type(e)
81 KnownFailureTest = type(e)
82
82
83 def addError(self, test, err, capt=None):
83 def addError(self, test, err, capt=None):
84 if issubclass(err[0], KnownFailureTest):
84 if issubclass(err[0], KnownFailureTest):
85 err = (SkipTest,) + err[1:]
85 err = (SkipTest,) + err[1:]
86 return self.orig_addError(test, err, capt)
86 return self.orig_addError(test, err, capt)
87
87
88 Xunit.orig_addError = Xunit.addError
88 Xunit.orig_addError = Xunit.addError
89 Xunit.addError = addError
89 Xunit.addError = addError
90
90
91 #-----------------------------------------------------------------------------
91 #-----------------------------------------------------------------------------
92 # Check which dependencies are installed and greater than minimum version.
92 # Check which dependencies are installed and greater than minimum version.
93 #-----------------------------------------------------------------------------
93 #-----------------------------------------------------------------------------
94 def extract_version(mod):
94 def extract_version(mod):
95 return mod.__version__
95 return mod.__version__
96
96
97 def test_for(item, min_version=None, callback=extract_version):
97 def test_for(item, min_version=None, callback=extract_version):
98 """Test to see if item is importable, and optionally check against a minimum
98 """Test to see if item is importable, and optionally check against a minimum
99 version.
99 version.
100
100
101 If min_version is given, the default behavior is to check against the
101 If min_version is given, the default behavior is to check against the
102 `__version__` attribute of the item, but specifying `callback` allows you to
102 `__version__` attribute of the item, but specifying `callback` allows you to
103 extract the value you are interested in. e.g::
103 extract the value you are interested in. e.g::
104
104
105 In [1]: import sys
105 In [1]: import sys
106
106
107 In [2]: from IPython.testing.iptest import test_for
107 In [2]: from IPython.testing.iptest import test_for
108
108
109 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
109 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
110 Out[3]: True
110 Out[3]: True
111
111
112 """
112 """
113 try:
113 try:
114 check = import_item(item)
114 check = import_item(item)
115 except (ImportError, RuntimeError):
115 except (ImportError, RuntimeError):
116 # GTK reports Runtime error if it can't be initialized even if it's
116 # GTK reports Runtime error if it can't be initialized even if it's
117 # importable.
117 # importable.
118 return False
118 return False
119 else:
119 else:
120 if min_version:
120 if min_version:
121 if callback:
121 if callback:
122 # extra processing step to get version to compare
122 # extra processing step to get version to compare
123 check = callback(check)
123 check = callback(check)
124
124
125 return check >= min_version
125 return check >= min_version
126 else:
126 else:
127 return True
127 return True
128
128
129 # Global dict where we can store information on what we have and what we don't
129 # Global dict where we can store information on what we have and what we don't
130 # have available at test run time
130 # have available at test run time
131 have = {}
131 have = {}
132
132
133 have['curses'] = test_for('_curses')
133 have['curses'] = test_for('_curses')
134 have['matplotlib'] = test_for('matplotlib')
134 have['matplotlib'] = test_for('matplotlib')
135 have['numpy'] = test_for('numpy')
135 have['numpy'] = test_for('numpy')
136 have['pexpect'] = test_for('IPython.external.pexpect')
136 have['pexpect'] = test_for('IPython.external.pexpect')
137 have['pymongo'] = test_for('pymongo')
137 have['pymongo'] = test_for('pymongo')
138 have['pygments'] = test_for('pygments')
138 have['pygments'] = test_for('pygments')
139 have['qt'] = test_for('IPython.external.qt')
139 have['qt'] = test_for('IPython.external.qt')
140 have['rpy2'] = test_for('rpy2')
140 have['rpy2'] = test_for('rpy2')
141 have['sqlite3'] = test_for('sqlite3')
141 have['sqlite3'] = test_for('sqlite3')
142 have['cython'] = test_for('Cython')
142 have['cython'] = test_for('Cython')
143 have['oct2py'] = test_for('oct2py')
143 have['oct2py'] = test_for('oct2py')
144 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
144 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
145 have['jinja2'] = test_for('jinja2')
145 have['jinja2'] = test_for('jinja2')
146 have['wx'] = test_for('wx')
146 have['wx'] = test_for('wx')
147 have['wx.aui'] = test_for('wx.aui')
147 have['wx.aui'] = test_for('wx.aui')
148 have['azure'] = test_for('azure')
148 have['azure'] = test_for('azure')
149 have['sphinx'] = test_for('sphinx')
149 have['sphinx'] = test_for('sphinx')
150
150
151 min_zmq = (2,1,11)
151 min_zmq = (2,1,11)
152
152
153 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
153 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
154
154
155 #-----------------------------------------------------------------------------
155 #-----------------------------------------------------------------------------
156 # Test suite definitions
156 # Test suite definitions
157 #-----------------------------------------------------------------------------
157 #-----------------------------------------------------------------------------
158
158
159 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
159 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
160 'extensions', 'lib', 'terminal', 'testing', 'utils',
160 'extensions', 'lib', 'terminal', 'testing', 'utils',
161 'nbformat', 'qt', 'html', 'nbconvert'
161 'nbformat', 'qt', 'html', 'nbconvert'
162 ]
162 ]
163
163
164 class TestSection(object):
164 class TestSection(object):
165 def __init__(self, name, includes):
165 def __init__(self, name, includes):
166 self.name = name
166 self.name = name
167 self.includes = includes
167 self.includes = includes
168 self.excludes = []
168 self.excludes = []
169 self.dependencies = []
169 self.dependencies = []
170 self.enabled = True
170 self.enabled = True
171
171
172 def exclude(self, module):
172 def exclude(self, module):
173 if not module.startswith('IPython'):
173 if not module.startswith('IPython'):
174 module = self.includes[0] + "." + module
174 module = self.includes[0] + "." + module
175 self.excludes.append(module.replace('.', os.sep))
175 self.excludes.append(module.replace('.', os.sep))
176
176
177 def requires(self, *packages):
177 def requires(self, *packages):
178 self.dependencies.extend(packages)
178 self.dependencies.extend(packages)
179
179
180 @property
180 @property
181 def will_run(self):
181 def will_run(self):
182 return self.enabled and all(have[p] for p in self.dependencies)
182 return self.enabled and all(have[p] for p in self.dependencies)
183
183
184 # Name -> (include, exclude, dependencies_met)
184 # Name -> (include, exclude, dependencies_met)
185 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
185 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
186
186
187 # Exclusions and dependencies
187 # Exclusions and dependencies
188 # ---------------------------
188 # ---------------------------
189
189
190 # core:
190 # core:
191 sec = test_sections['core']
191 sec = test_sections['core']
192 if not have['sqlite3']:
192 if not have['sqlite3']:
193 sec.exclude('tests.test_history')
193 sec.exclude('tests.test_history')
194 sec.exclude('history')
194 sec.exclude('history')
195 if not have['matplotlib']:
195 if not have['matplotlib']:
196 sec.exclude('pylabtools'),
196 sec.exclude('pylabtools'),
197 sec.exclude('tests.test_pylabtools')
197 sec.exclude('tests.test_pylabtools')
198
198
199 # lib:
199 # lib:
200 sec = test_sections['lib']
200 sec = test_sections['lib']
201 if not have['wx']:
201 if not have['wx']:
202 sec.exclude('inputhookwx')
202 sec.exclude('inputhookwx')
203 if not have['pexpect']:
203 if not have['pexpect']:
204 sec.exclude('irunner')
204 sec.exclude('irunner')
205 sec.exclude('tests.test_irunner')
205 sec.exclude('tests.test_irunner')
206 if not have['zmq']:
206 if not have['zmq']:
207 sec.exclude('kernel')
207 sec.exclude('kernel')
208 # We do this unconditionally, so that the test suite doesn't import
208 # We do this unconditionally, so that the test suite doesn't import
209 # gtk, changing the default encoding and masking some unicode bugs.
209 # gtk, changing the default encoding and masking some unicode bugs.
210 sec.exclude('inputhookgtk')
210 sec.exclude('inputhookgtk')
211 # Testing inputhook will need a lot of thought, to figure out
211 # Testing inputhook will need a lot of thought, to figure out
212 # how to have tests that don't lock up with the gui event
212 # how to have tests that don't lock up with the gui event
213 # loops in the picture
213 # loops in the picture
214 sec.exclude('inputhook')
214 sec.exclude('inputhook')
215
215
216 # testing:
216 # testing:
217 sec = test_sections['lib']
217 sec = test_sections['lib']
218 # This guy is probably attic material
218 # This guy is probably attic material
219 sec.exclude('mkdoctests')
219 sec.exclude('mkdoctests')
220 # These have to be skipped on win32 because the use echo, rm, cd, etc.
220 # These have to be skipped on win32 because the use echo, rm, cd, etc.
221 # See ticket https://github.com/ipython/ipython/issues/87
221 # See ticket https://github.com/ipython/ipython/issues/87
222 if sys.platform == 'win32':
222 if sys.platform == 'win32':
223 sec.exclude('plugin.test_exampleip')
223 sec.exclude('plugin.test_exampleip')
224 sec.exclude('plugin.dtexample')
224 sec.exclude('plugin.dtexample')
225
225
226 # terminal:
226 # terminal:
227 if (not have['pexpect']) or (not have['zmq']):
227 if (not have['pexpect']) or (not have['zmq']):
228 test_sections['terminal'].exclude('console')
228 test_sections['terminal'].exclude('console')
229
229
230 # parallel
230 # parallel
231 sec = test_sections['parallel']
231 sec = test_sections['parallel']
232 sec.requires('zmq')
232 sec.requires('zmq')
233 if not have['pymongo']:
233 if not have['pymongo']:
234 sec.exclude('controller.mongodb')
234 sec.exclude('controller.mongodb')
235 sec.exclude('tests.test_mongodb')
235 sec.exclude('tests.test_mongodb')
236
236
237 # kernel:
237 # kernel:
238 sec = test_sections['kernel']
238 sec = test_sections['kernel']
239 sec.requires('zmq')
239 sec.requires('zmq')
240 # The in-process kernel tests are done in a separate section
240 # The in-process kernel tests are done in a separate section
241 sec.exclude('inprocess')
241 sec.exclude('inprocess')
242 # importing gtk sets the default encoding, which we want to avoid
242 # importing gtk sets the default encoding, which we want to avoid
243 sec.exclude('zmq.gui.gtkembed')
243 sec.exclude('zmq.gui.gtkembed')
244 if not have['matplotlib']:
244 if not have['matplotlib']:
245 sec.exclude('zmq.pylab')
245 sec.exclude('zmq.pylab')
246
246
247 # kernel.inprocess:
247 # kernel.inprocess:
248 test_sections['kernel.inprocess'].requires('zmq')
248 test_sections['kernel.inprocess'].requires('zmq')
249
249
250 # extensions:
250 # extensions:
251 sec = test_sections['extensions']
251 sec = test_sections['extensions']
252 if not have['cython']:
252 if not have['cython']:
253 sec.exclude('cythonmagic')
253 sec.exclude('cythonmagic')
254 sec.exclude('tests.test_cythonmagic')
254 sec.exclude('tests.test_cythonmagic')
255 if not have['oct2py']:
255 if not have['oct2py']:
256 sec.exclude('octavemagic')
256 sec.exclude('octavemagic')
257 sec.exclude('tests.test_octavemagic')
257 sec.exclude('tests.test_octavemagic')
258 if not have['rpy2'] or not have['numpy']:
258 if not have['rpy2'] or not have['numpy']:
259 sec.exclude('rmagic')
259 sec.exclude('rmagic')
260 sec.exclude('tests.test_rmagic')
260 sec.exclude('tests.test_rmagic')
261 # autoreload does some strange stuff, so move it to its own test section
261 # autoreload does some strange stuff, so move it to its own test section
262 sec.exclude('autoreload')
262 sec.exclude('autoreload')
263 sec.exclude('tests.test_autoreload')
263 sec.exclude('tests.test_autoreload')
264 test_sections['autoreload'] = TestSection('autoreload',
264 test_sections['autoreload'] = TestSection('autoreload',
265 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
265 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
266 test_group_names.append('autoreload')
266 test_group_names.append('autoreload')
267
267
268 # qt:
268 # qt:
269 test_sections['qt'].requires('zmq', 'qt', 'pygments')
269 test_sections['qt'].requires('zmq', 'qt', 'pygments')
270
270
271 # html:
271 # html:
272 sec = test_sections['html']
272 sec = test_sections['html']
273 sec.requires('zmq', 'tornado')
273 sec.requires('zmq', 'tornado')
274 # The notebook 'static' directory contains JS, css and other
274 # The notebook 'static' directory contains JS, css and other
275 # files for web serving. Occasionally projects may put a .py
275 # files for web serving. Occasionally projects may put a .py
276 # file in there (MathJax ships a conf.py), so we might as
276 # file in there (MathJax ships a conf.py), so we might as
277 # well play it safe and skip the whole thing.
277 # well play it safe and skip the whole thing.
278 sec.exclude('static')
278 sec.exclude('static')
279 sec.exclude('fabfile')
279 sec.exclude('fabfile')
280 if not have['jinja2']:
280 if not have['jinja2']:
281 sec.exclude('notebookapp')
281 sec.exclude('notebookapp')
282 if not have['azure']:
282 if not have['azure']:
283 sec.exclude('services.notebooks.azurenbmanager')
283 sec.exclude('services.notebooks.azurenbmanager')
284
284
285 # config:
285 # config:
286 # Config files aren't really importable stand-alone
286 # Config files aren't really importable stand-alone
287 test_sections['config'].exclude('profile')
287 test_sections['config'].exclude('profile')
288
288
289 # nbconvert:
289 # nbconvert:
290 sec = test_sections['nbconvert']
290 sec = test_sections['nbconvert']
291 sec.requires('pygments', 'jinja2', 'sphinx')
291 sec.requires('pygments', 'jinja2', 'sphinx')
292 # Exclude nbconvert directories containing config files used to test.
292 # Exclude nbconvert directories containing config files used to test.
293 # Executing the config files with iptest would cause an exception.
293 # Executing the config files with iptest would cause an exception.
294 sec.exclude('tests.files')
294 sec.exclude('tests.files')
295 sec.exclude('exporters.tests.files')
295 sec.exclude('exporters.tests.files')
296
296
297 #-----------------------------------------------------------------------------
297 #-----------------------------------------------------------------------------
298 # Functions and classes
298 # Functions and classes
299 #-----------------------------------------------------------------------------
299 #-----------------------------------------------------------------------------
300
300
301 def check_exclusions_exist():
301 def check_exclusions_exist():
302 from IPython.utils.path import get_ipython_package_dir
302 from IPython.utils.path import get_ipython_package_dir
303 from IPython.utils.warn import warn
303 from IPython.utils.warn import warn
304 parent = os.path.dirname(get_ipython_package_dir())
304 parent = os.path.dirname(get_ipython_package_dir())
305 for sec in test_sections:
305 for sec in test_sections:
306 for pattern in sec.exclusions:
306 for pattern in sec.exclusions:
307 fullpath = pjoin(parent, pattern)
307 fullpath = pjoin(parent, pattern)
308 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
308 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
309 warn("Excluding nonexistent file: %r" % pattern)
309 warn("Excluding nonexistent file: %r" % pattern)
310
310
311
311
312 class ExclusionPlugin(Plugin):
312 class ExclusionPlugin(Plugin):
313 """A nose plugin to effect our exclusions of files and directories.
313 """A nose plugin to effect our exclusions of files and directories.
314 """
314 """
315 name = 'exclusions'
315 name = 'exclusions'
316 score = 3000 # Should come before any other plugins
316 score = 3000 # Should come before any other plugins
317
317
318 def __init__(self, exclude_patterns=None):
318 def __init__(self, exclude_patterns=None):
319 """
319 """
320 Parameters
320 Parameters
321 ----------
321 ----------
322
322
323 exclude_patterns : sequence of strings, optional
323 exclude_patterns : sequence of strings, optional
324 These patterns are compiled as regular expressions, subsequently used
324 These patterns are compiled as regular expressions, subsequently used
325 to exclude any filename which matches them from inclusion in the test
325 to exclude any filename which matches them from inclusion in the test
326 suite (using pattern.search(), NOT pattern.match() ).
326 suite (using pattern.search(), NOT pattern.match() ).
327 """
327 """
328
328
329 if exclude_patterns is None:
329 if exclude_patterns is None:
330 exclude_patterns = []
330 exclude_patterns = []
331 self.exclude_patterns = [re.compile(p) for p in exclude_patterns]
331 self.exclude_patterns = [re.compile(p) for p in exclude_patterns]
332 super(ExclusionPlugin, self).__init__()
332 super(ExclusionPlugin, self).__init__()
333
333
334 def options(self, parser, env=os.environ):
334 def options(self, parser, env=os.environ):
335 Plugin.options(self, parser, env)
335 Plugin.options(self, parser, env)
336
336
337 def configure(self, options, config):
337 def configure(self, options, config):
338 Plugin.configure(self, options, config)
338 Plugin.configure(self, options, config)
339 # Override nose trying to disable plugin.
339 # Override nose trying to disable plugin.
340 self.enabled = True
340 self.enabled = True
341
341
342 def wantFile(self, filename):
342 def wantFile(self, filename):
343 """Return whether the given filename should be scanned for tests.
343 """Return whether the given filename should be scanned for tests.
344 """
344 """
345 if any(pat.search(filename) for pat in self.exclude_patterns):
345 if any(pat.search(filename) for pat in self.exclude_patterns):
346 return False
346 return False
347 return None
347 return None
348
348
349 def wantDirectory(self, directory):
349 def wantDirectory(self, directory):
350 """Return whether the given directory should be scanned for tests.
350 """Return whether the given directory should be scanned for tests.
351 """
351 """
352 if any(pat.search(directory) for pat in self.exclude_patterns):
352 if any(pat.search(directory) for pat in self.exclude_patterns):
353 return False
353 return False
354 return None
354 return None
355
355
356
356
357 def run_iptest():
357 def run_iptest():
358 """Run the IPython test suite using nose.
358 """Run the IPython test suite using nose.
359
359
360 This function is called when this script is **not** called with the form
360 This function is called when this script is **not** called with the form
361 `iptest all`. It simply calls nose with appropriate command line flags
361 `iptest all`. It simply calls nose with appropriate command line flags
362 and accepts all of the standard nose arguments.
362 and accepts all of the standard nose arguments.
363 """
363 """
364 # Apply our monkeypatch to Xunit
364 # Apply our monkeypatch to Xunit
365 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
365 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
366 monkeypatch_xunit()
366 monkeypatch_xunit()
367
367
368 warnings.filterwarnings('ignore',
368 warnings.filterwarnings('ignore',
369 'This will be removed soon. Use IPython.testing.util instead')
369 'This will be removed soon. Use IPython.testing.util instead')
370
370
371 section = test_sections[sys.argv[1]]
371 if sys.argv[1] in test_sections:
372 sys.argv[1:2] = section.includes
372 section = test_sections[sys.argv[1]]
373 sys.argv[1:2] = section.includes
374 else:
375 arg1 = sys.argv[1]
376 section = TestSection(arg1, includes=[arg1])
377
373
378
374 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
379 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
375
380
376 '--with-ipdoctest',
381 '--with-ipdoctest',
377 '--ipdoctest-tests','--ipdoctest-extension=txt',
382 '--ipdoctest-tests','--ipdoctest-extension=txt',
378
383
379 # We add --exe because of setuptools' imbecility (it
384 # We add --exe because of setuptools' imbecility (it
380 # blindly does chmod +x on ALL files). Nose does the
385 # blindly does chmod +x on ALL files). Nose does the
381 # right thing and it tries to avoid executables,
386 # right thing and it tries to avoid executables,
382 # setuptools unfortunately forces our hand here. This
387 # setuptools unfortunately forces our hand here. This
383 # has been discussed on the distutils list and the
388 # has been discussed on the distutils list and the
384 # setuptools devs refuse to fix this problem!
389 # setuptools devs refuse to fix this problem!
385 '--exe',
390 '--exe',
386 ]
391 ]
387 if '-a' not in argv and '-A' not in argv:
392 if '-a' not in argv and '-A' not in argv:
388 argv = argv + ['-a', '!crash']
393 argv = argv + ['-a', '!crash']
389
394
390 if nose.__version__ >= '0.11':
395 if nose.__version__ >= '0.11':
391 # I don't fully understand why we need this one, but depending on what
396 # I don't fully understand why we need this one, but depending on what
392 # directory the test suite is run from, if we don't give it, 0 tests
397 # directory the test suite is run from, if we don't give it, 0 tests
393 # get run. Specifically, if the test suite is run from the source dir
398 # get run. Specifically, if the test suite is run from the source dir
394 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
399 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
395 # even if the same call done in this directory works fine). It appears
400 # even if the same call done in this directory works fine). It appears
396 # that if the requested package is in the current dir, nose bails early
401 # that if the requested package is in the current dir, nose bails early
397 # by default. Since it's otherwise harmless, leave it in by default
402 # by default. Since it's otherwise harmless, leave it in by default
398 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
403 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
399 argv.append('--traverse-namespace')
404 argv.append('--traverse-namespace')
400
405
401 # use our plugin for doctesting. It will remove the standard doctest plugin
406 # use our plugin for doctesting. It will remove the standard doctest plugin
402 # if it finds it enabled
407 # if it finds it enabled
403 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure()]
408 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure()]
404
409
405 # Use working directory set by parent process (see iptestcontroller)
410 # Use working directory set by parent process (see iptestcontroller)
406 if 'IPTEST_WORKING_DIR' in os.environ:
411 if 'IPTEST_WORKING_DIR' in os.environ:
407 os.chdir(os.environ['IPTEST_WORKING_DIR'])
412 os.chdir(os.environ['IPTEST_WORKING_DIR'])
408
413
409 # We need a global ipython running in this process, but the special
414 # We need a global ipython running in this process, but the special
410 # in-process group spawns its own IPython kernels, so for *that* group we
415 # in-process group spawns its own IPython kernels, so for *that* group we
411 # must avoid also opening the global one (otherwise there's a conflict of
416 # must avoid also opening the global one (otherwise there's a conflict of
412 # singletons). Ultimately the solution to this problem is to refactor our
417 # singletons). Ultimately the solution to this problem is to refactor our
413 # assumptions about what needs to be a singleton and what doesn't (app
418 # assumptions about what needs to be a singleton and what doesn't (app
414 # objects should, individual shells shouldn't). But for now, this
419 # objects should, individual shells shouldn't). But for now, this
415 # workaround allows the test suite for the inprocess module to complete.
420 # workaround allows the test suite for the inprocess module to complete.
416 if section.name != 'kernel.inprocess':
421 if 'kernel.inprocess' not in section.name:
417 from IPython.testing import globalipapp
422 from IPython.testing import globalipapp
418 globalipapp.start_ipython()
423 globalipapp.start_ipython()
419
424
420 # Now nose can run
425 # Now nose can run
421 TestProgram(argv=argv, addplugins=plugins)
426 TestProgram(argv=argv, addplugins=plugins)
422
427
423 if __name__ == '__main__':
428 if __name__ == '__main__':
424 run_iptest()
429 run_iptest()
425
430
@@ -1,382 +1,383 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
2 """IPython Test Process Controller
3
3
4 This module runs one or more subprocesses which will actually run the IPython
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
5 test suite.
6
6
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2009-2011 The IPython Development Team
10 # Copyright (C) 2009-2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
19 from __future__ import print_function
20
20
21 import argparse
21 import argparse
22 import multiprocessing.pool
22 import multiprocessing.pool
23 import os
23 import os
24 import shutil
24 import shutil
25 import signal
25 import signal
26 import sys
26 import sys
27 import subprocess
27 import subprocess
28 import time
28 import time
29
29
30 from .iptest import have, test_group_names, test_sections
30 from .iptest import have, test_group_names, test_sections
31 from IPython.utils.py3compat import bytes_to_str
31 from IPython.utils.py3compat import bytes_to_str
32 from IPython.utils.sysinfo import sys_info
32 from IPython.utils.sysinfo import sys_info
33 from IPython.utils.tempdir import TemporaryDirectory
33 from IPython.utils.tempdir import TemporaryDirectory
34
34
35
35
36 class TestController(object):
36 class TestController(object):
37 """Run tests in a subprocess
37 """Run tests in a subprocess
38 """
38 """
39 #: str, IPython test suite to be executed.
39 #: str, IPython test suite to be executed.
40 section = None
40 section = None
41 #: list, command line arguments to be executed
41 #: list, command line arguments to be executed
42 cmd = None
42 cmd = None
43 #: dict, extra environment variables to set for the subprocess
43 #: dict, extra environment variables to set for the subprocess
44 env = None
44 env = None
45 #: list, TemporaryDirectory instances to clear up when the process finishes
45 #: list, TemporaryDirectory instances to clear up when the process finishes
46 dirs = None
46 dirs = None
47 #: subprocess.Popen instance
47 #: subprocess.Popen instance
48 process = None
48 process = None
49 #: str, process stdout+stderr
49 #: str, process stdout+stderr
50 stdout = None
50 stdout = None
51 #: bool, whether to capture process stdout & stderr
51 #: bool, whether to capture process stdout & stderr
52 buffer_output = False
52 buffer_output = False
53
53
54 def __init__(self):
54 def __init__(self):
55 self.cmd = []
55 self.cmd = []
56 self.env = {}
56 self.env = {}
57 self.dirs = []
57 self.dirs = []
58
58
59 @property
59 @property
60 def will_run(self):
60 def will_run(self):
61 """Override in subclasses to check for dependencies."""
61 """Override in subclasses to check for dependencies."""
62 return False
62 return False
63
63
64 def launch(self):
64 def launch(self):
65 # print('*** ENV:', self.env) # dbg
65 # print('*** ENV:', self.env) # dbg
66 # print('*** CMD:', self.cmd) # dbg
66 # print('*** CMD:', self.cmd) # dbg
67 env = os.environ.copy()
67 env = os.environ.copy()
68 env.update(self.env)
68 env.update(self.env)
69 output = subprocess.PIPE if self.buffer_output else None
69 output = subprocess.PIPE if self.buffer_output else None
70 stdout = subprocess.STDOUT if self.buffer_output else None
70 stdout = subprocess.STDOUT if self.buffer_output else None
71 self.process = subprocess.Popen(self.cmd, stdout=output,
71 self.process = subprocess.Popen(self.cmd, stdout=output,
72 stderr=stdout, env=env)
72 stderr=stdout, env=env)
73
73
74 def wait(self):
74 def wait(self):
75 self.stdout, _ = self.process.communicate()
75 self.stdout, _ = self.process.communicate()
76 return self.process.returncode
76 return self.process.returncode
77
77
78 def cleanup_process(self):
78 def cleanup_process(self):
79 """Cleanup on exit by killing any leftover processes."""
79 """Cleanup on exit by killing any leftover processes."""
80 subp = self.process
80 subp = self.process
81 if subp is None or (subp.poll() is not None):
81 if subp is None or (subp.poll() is not None):
82 return # Process doesn't exist, or is already dead.
82 return # Process doesn't exist, or is already dead.
83
83
84 try:
84 try:
85 print('Cleaning up stale PID: %d' % subp.pid)
85 print('Cleaning up stale PID: %d' % subp.pid)
86 subp.kill()
86 subp.kill()
87 except: # (OSError, WindowsError) ?
87 except: # (OSError, WindowsError) ?
88 # This is just a best effort, if we fail or the process was
88 # This is just a best effort, if we fail or the process was
89 # really gone, ignore it.
89 # really gone, ignore it.
90 pass
90 pass
91 else:
91 else:
92 for i in range(10):
92 for i in range(10):
93 if subp.poll() is None:
93 if subp.poll() is None:
94 time.sleep(0.1)
94 time.sleep(0.1)
95 else:
95 else:
96 break
96 break
97
97
98 if subp.poll() is None:
98 if subp.poll() is None:
99 # The process did not die...
99 # The process did not die...
100 print('... failed. Manual cleanup may be required.')
100 print('... failed. Manual cleanup may be required.')
101
101
102 def cleanup(self):
102 def cleanup(self):
103 "Kill process if it's still alive, and clean up temporary directories"
103 "Kill process if it's still alive, and clean up temporary directories"
104 self.cleanup_process()
104 self.cleanup_process()
105 for td in self.dirs:
105 for td in self.dirs:
106 td.cleanup()
106 td.cleanup()
107
107
108 __del__ = cleanup
108 __del__ = cleanup
109
109
110 class PyTestController(TestController):
110 class PyTestController(TestController):
111 """Run Python tests using IPython.testing.iptest"""
111 """Run Python tests using IPython.testing.iptest"""
112 #: str, Python command to execute in subprocess
112 #: str, Python command to execute in subprocess
113 pycmd = None
113 pycmd = None
114
114
115 def __init__(self, section):
115 def __init__(self, section):
116 """Create new test runner."""
116 """Create new test runner."""
117 TestController.__init__(self)
117 TestController.__init__(self)
118 self.section = section
118 self.section = section
119 # pycmd is put into cmd[2] in PyTestController.launch()
119 # pycmd is put into cmd[2] in PyTestController.launch()
120 self.cmd = [sys.executable, '-c', None, section]
120 self.cmd = [sys.executable, '-c', None, section]
121 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
121 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
122 ipydir = TemporaryDirectory()
122 ipydir = TemporaryDirectory()
123 self.dirs.append(ipydir)
123 self.dirs.append(ipydir)
124 self.env['IPYTHONDIR'] = ipydir.name
124 self.env['IPYTHONDIR'] = ipydir.name
125 self.workingdir = workingdir = TemporaryDirectory()
125 self.workingdir = workingdir = TemporaryDirectory()
126 self.dirs.append(workingdir)
126 self.dirs.append(workingdir)
127 self.env['IPTEST_WORKING_DIR'] = workingdir.name
127 self.env['IPTEST_WORKING_DIR'] = workingdir.name
128 # This means we won't get odd effects from our own matplotlib config
128 # This means we won't get odd effects from our own matplotlib config
129 self.env['MPLCONFIGDIR'] = workingdir.name
129 self.env['MPLCONFIGDIR'] = workingdir.name
130
130
131 @property
131 @property
132 def will_run(self):
132 def will_run(self):
133 return test_sections[self.section].will_run
133 return test_sections[self.section].will_run
134
134
135 def add_xunit(self):
135 def add_xunit(self):
136 xunit_file = os.path.abspath(self.section + '.xunit.xml')
136 xunit_file = os.path.abspath(self.section + '.xunit.xml')
137 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
137 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
138
138
139 def add_coverage(self):
139 def add_coverage(self):
140 coverage_rc = ("[run]\n"
140 coverage_rc = ("[run]\n"
141 "data_file = {data_file}\n"
141 "data_file = {data_file}\n"
142 "source =\n"
142 "source =\n"
143 " {source}\n"
143 " {source}\n"
144 ).format(data_file=os.path.abspath('.coverage.'+self.section),
144 ).format(data_file=os.path.abspath('.coverage.'+self.section),
145 source="\n ".join(test_sections[self.section].includes))
145 source="\n ".join(test_sections[self.section].includes))
146
146
147 config_file = os.path.join(self.workingdir.name, '.coveragerc')
147 config_file = os.path.join(self.workingdir.name, '.coveragerc')
148 with open(config_file, 'w') as f:
148 with open(config_file, 'w') as f:
149 f.write(coverage_rc)
149 f.write(coverage_rc)
150
150
151 self.env['COVERAGE_PROCESS_START'] = config_file
151 self.env['COVERAGE_PROCESS_START'] = config_file
152 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
152 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
153
153
154 def launch(self):
154 def launch(self):
155 self.cmd[2] = self.pycmd
155 self.cmd[2] = self.pycmd
156 super(PyTestController, self).launch()
156 super(PyTestController, self).launch()
157
157
158
158
159 def prepare_py_test_controllers(inc_slow=False, xunit=False, coverage=False):
159 def prepare_py_test_controllers(inc_slow=False, xunit=False, coverage=False):
160 """Returns an ordered list of PyTestController instances to be run."""
160 """Returns an ordered list of PyTestController instances to be run."""
161 to_run, not_run = [], []
161 to_run, not_run = [], []
162 if not inc_slow:
162 if not inc_slow:
163 test_sections['parallel'].enabled = False
163 test_sections['parallel'].enabled = False
164
164
165 for name in test_group_names:
165 for name in test_group_names:
166 controller = PyTestController(name)
166 controller = PyTestController(name)
167 if xunit:
167 if xunit:
168 controller.add_xunit()
168 controller.add_xunit()
169 if coverage:
169 if coverage:
170 controller.add_coverage()
170 controller.add_coverage()
171 if controller.will_run:
171 if controller.will_run:
172 to_run.append(controller)
172 to_run.append(controller)
173 else:
173 else:
174 not_run.append(controller)
174 not_run.append(controller)
175 return to_run, not_run
175 return to_run, not_run
176
176
177 def do_run(controller):
177 def do_run(controller):
178 try:
178 try:
179 try:
179 try:
180 controller.launch()
180 controller.launch()
181 except Exception:
181 except Exception:
182 import traceback
182 import traceback
183 traceback.print_exc()
183 traceback.print_exc()
184 return controller, 1 # signal failure
184 return controller, 1 # signal failure
185
185
186 exitcode = controller.wait()
186 exitcode = controller.wait()
187 return controller, exitcode
187 return controller, exitcode
188
188
189 except KeyboardInterrupt:
189 except KeyboardInterrupt:
190 return controller, -signal.SIGINT
190 return controller, -signal.SIGINT
191 finally:
191 finally:
192 controller.cleanup()
192 controller.cleanup()
193
193
194 def report():
194 def report():
195 """Return a string with a summary report of test-related variables."""
195 """Return a string with a summary report of test-related variables."""
196
196
197 out = [ sys_info(), '\n']
197 out = [ sys_info(), '\n']
198
198
199 avail = []
199 avail = []
200 not_avail = []
200 not_avail = []
201
201
202 for k, is_avail in have.items():
202 for k, is_avail in have.items():
203 if is_avail:
203 if is_avail:
204 avail.append(k)
204 avail.append(k)
205 else:
205 else:
206 not_avail.append(k)
206 not_avail.append(k)
207
207
208 if avail:
208 if avail:
209 out.append('\nTools and libraries available at test time:\n')
209 out.append('\nTools and libraries available at test time:\n')
210 avail.sort()
210 avail.sort()
211 out.append(' ' + ' '.join(avail)+'\n')
211 out.append(' ' + ' '.join(avail)+'\n')
212
212
213 if not_avail:
213 if not_avail:
214 out.append('\nTools and libraries NOT available at test time:\n')
214 out.append('\nTools and libraries NOT available at test time:\n')
215 not_avail.sort()
215 not_avail.sort()
216 out.append(' ' + ' '.join(not_avail)+'\n')
216 out.append(' ' + ' '.join(not_avail)+'\n')
217
217
218 return ''.join(out)
218 return ''.join(out)
219
219
220 def run_iptestall(inc_slow=False, jobs=1, xunit_out=False, coverage_out=False):
220 def run_iptestall(inc_slow=False, jobs=1, xunit_out=False, coverage_out=False):
221 """Run the entire IPython test suite by calling nose and trial.
221 """Run the entire IPython test suite by calling nose and trial.
222
222
223 This function constructs :class:`IPTester` instances for all IPython
223 This function constructs :class:`IPTester` instances for all IPython
224 modules and package and then runs each of them. This causes the modules
224 modules and package and then runs each of them. This causes the modules
225 and packages of IPython to be tested each in their own subprocess using
225 and packages of IPython to be tested each in their own subprocess using
226 nose.
226 nose.
227
227
228 Parameters
228 Parameters
229 ----------
229 ----------
230
230
231 inc_slow : bool, optional
231 inc_slow : bool, optional
232 Include slow tests, like IPython.parallel. By default, these tests aren't
232 Include slow tests, like IPython.parallel. By default, these tests aren't
233 run.
233 run.
234
234
235 fast : bool, option
235 fast : bool, option
236 Run the test suite in parallel, if True, using as many threads as there
236 Run the test suite in parallel, if True, using as many threads as there
237 are processors
237 are processors
238 """
238 """
239 if jobs != 1:
239 if jobs != 1:
240 TestController.buffer_output = True
240 TestController.buffer_output = True
241
241
242 to_run, not_run = prepare_py_test_controllers(inc_slow=inc_slow, xunit=xunit_out,
242 to_run, not_run = prepare_py_test_controllers(inc_slow=inc_slow, xunit=xunit_out,
243 coverage=coverage_out)
243 coverage=coverage_out)
244
244
245 def justify(ltext, rtext, width=70, fill='-'):
245 def justify(ltext, rtext, width=70, fill='-'):
246 ltext += ' '
246 ltext += ' '
247 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
247 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
248 return ltext + rtext
248 return ltext + rtext
249
249
250 # Run all test runners, tracking execution time
250 # Run all test runners, tracking execution time
251 failed = []
251 failed = []
252 t_start = time.time()
252 t_start = time.time()
253
253
254 print('*'*70)
254 print('*'*70)
255 if jobs == 1:
255 if jobs == 1:
256 for controller in to_run:
256 for controller in to_run:
257 print('IPython test group:', controller.section)
257 print('IPython test group:', controller.section)
258 controller, res = do_run(controller)
258 controller, res = do_run(controller)
259 if res:
259 if res:
260 failed.append(controller)
260 failed.append(controller)
261 if res == -signal.SIGINT:
261 if res == -signal.SIGINT:
262 print("Interrupted")
262 print("Interrupted")
263 break
263 break
264 print()
264 print()
265
265
266 else:
266 else:
267 try:
267 try:
268 pool = multiprocessing.pool.ThreadPool(jobs)
268 pool = multiprocessing.pool.ThreadPool(jobs)
269 for (controller, res) in pool.imap_unordered(do_run, to_run):
269 for (controller, res) in pool.imap_unordered(do_run, to_run):
270 res_string = 'OK' if res == 0 else 'FAILED'
270 res_string = 'OK' if res == 0 else 'FAILED'
271 print(justify('IPython test group: ' + controller.section, res_string))
271 print(justify('IPython test group: ' + controller.section, res_string))
272 if res:
272 if res:
273 print(bytes_to_str(controller.stdout))
273 print(bytes_to_str(controller.stdout))
274 failed.append(controller)
274 failed.append(controller)
275 if res == -signal.SIGINT:
275 if res == -signal.SIGINT:
276 print("Interrupted")
276 print("Interrupted")
277 break
277 break
278 except KeyboardInterrupt:
278 except KeyboardInterrupt:
279 return
279 return
280
280
281 for controller in not_run:
281 for controller in not_run:
282 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
282 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
283
283
284 t_end = time.time()
284 t_end = time.time()
285 t_tests = t_end - t_start
285 t_tests = t_end - t_start
286 nrunners = len(to_run)
286 nrunners = len(to_run)
287 nfail = len(failed)
287 nfail = len(failed)
288 # summarize results
288 # summarize results
289 print('*'*70)
289 print('*'*70)
290 print('Test suite completed for system with the following information:')
290 print('Test suite completed for system with the following information:')
291 print(report())
291 print(report())
292 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
292 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
293 print()
293 print()
294 print('Status:')
294 print('Status:')
295 if not failed:
295 if not failed:
296 print('OK')
296 print('OK')
297 else:
297 else:
298 # If anything went wrong, point out what command to rerun manually to
298 # If anything went wrong, point out what command to rerun manually to
299 # see the actual errors and individual summary
299 # see the actual errors and individual summary
300 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
300 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
301 for controller in failed:
301 for controller in failed:
302 print('-'*40)
302 print('-'*40)
303 print('Runner failed:', controller.section)
303 print('Runner failed:', controller.section)
304 print('You may wish to rerun this one individually, with:')
304 print('You may wish to rerun this one individually, with:')
305 print(' iptest', *controller.cmd[3:])
305 print(' iptest', *controller.cmd[3:])
306 print()
306 print()
307
307
308 if coverage_out:
308 if coverage_out:
309 from coverage import coverage
309 from coverage import coverage
310 cov = coverage(data_file='.coverage')
310 cov = coverage(data_file='.coverage')
311 cov.combine()
311 cov.combine()
312 cov.save()
312 cov.save()
313
313
314 # Coverage HTML report
314 # Coverage HTML report
315 if coverage_out == 'html':
315 if coverage_out == 'html':
316 html_dir = 'ipy_htmlcov'
316 html_dir = 'ipy_htmlcov'
317 shutil.rmtree(html_dir, ignore_errors=True)
317 shutil.rmtree(html_dir, ignore_errors=True)
318 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
318 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
319 sys.stdout.flush()
319 sys.stdout.flush()
320
320
321 # Custom HTML reporter to clean up module names.
321 # Custom HTML reporter to clean up module names.
322 from coverage.html import HtmlReporter
322 from coverage.html import HtmlReporter
323 class CustomHtmlReporter(HtmlReporter):
323 class CustomHtmlReporter(HtmlReporter):
324 def find_code_units(self, morfs):
324 def find_code_units(self, morfs):
325 super(CustomHtmlReporter, self).find_code_units(morfs)
325 super(CustomHtmlReporter, self).find_code_units(morfs)
326 for cu in self.code_units:
326 for cu in self.code_units:
327 nameparts = cu.name.split(os.sep)
327 nameparts = cu.name.split(os.sep)
328 if 'IPython' not in nameparts:
328 if 'IPython' not in nameparts:
329 continue
329 continue
330 ix = nameparts.index('IPython')
330 ix = nameparts.index('IPython')
331 cu.name = '.'.join(nameparts[ix:])
331 cu.name = '.'.join(nameparts[ix:])
332
332
333 # Reimplement the html_report method with our custom reporter
333 # Reimplement the html_report method with our custom reporter
334 cov._harvest_data()
334 cov._harvest_data()
335 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
335 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
336 html_title='IPython test coverage',
336 html_title='IPython test coverage',
337 )
337 )
338 reporter = CustomHtmlReporter(cov, cov.config)
338 reporter = CustomHtmlReporter(cov, cov.config)
339 reporter.report(None)
339 reporter.report(None)
340 print('done.')
340 print('done.')
341
341
342 # Coverage XML report
342 # Coverage XML report
343 elif coverage_out == 'xml':
343 elif coverage_out == 'xml':
344 cov.xml_report(outfile='ipy_coverage.xml')
344 cov.xml_report(outfile='ipy_coverage.xml')
345
345
346 if failed:
346 if failed:
347 # Ensure that our exit code indicates failure
347 # Ensure that our exit code indicates failure
348 sys.exit(1)
348 sys.exit(1)
349
349
350
350
351 def main():
351 def main():
352 if len(sys.argv) > 1 and (sys.argv[1] in test_sections):
352 if len(sys.argv) > 1 and \
353 ((sys.argv[1] in test_sections) or sys.argv[1].startswith('IPython')):
353 from .iptest import run_iptest
354 from .iptest import run_iptest
354 # This is in-process
355 # This is in-process
355 run_iptest()
356 run_iptest()
356 return
357 return
357
358
358 parser = argparse.ArgumentParser(description='Run IPython test suite')
359 parser = argparse.ArgumentParser(description='Run IPython test suite')
359 parser.add_argument('--all', action='store_true',
360 parser.add_argument('--all', action='store_true',
360 help='Include slow tests not run by default.')
361 help='Include slow tests not run by default.')
361 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
362 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
362 help='Run test sections in parallel.')
363 help='Run test sections in parallel.')
363 parser.add_argument('--xunit', action='store_true',
364 parser.add_argument('--xunit', action='store_true',
364 help='Produce Xunit XML results')
365 help='Produce Xunit XML results')
365 parser.add_argument('--coverage', nargs='?', const=True, default=False,
366 parser.add_argument('--coverage', nargs='?', const=True, default=False,
366 help="Measure test coverage. Specify 'html' or "
367 help="Measure test coverage. Specify 'html' or "
367 "'xml' to get reports.")
368 "'xml' to get reports.")
368
369
369 options = parser.parse_args()
370 options = parser.parse_args()
370
371
371 try:
372 try:
372 jobs = int(options.fast)
373 jobs = int(options.fast)
373 except TypeError:
374 except TypeError:
374 jobs = options.fast
375 jobs = options.fast
375
376
376 # This starts subprocesses
377 # This starts subprocesses
377 run_iptestall(inc_slow=options.all, jobs=jobs,
378 run_iptestall(inc_slow=options.all, jobs=jobs,
378 xunit_out=options.xunit, coverage_out=options.coverage)
379 xunit_out=options.xunit, coverage_out=options.coverage)
379
380
380
381
381 if __name__ == '__main__':
382 if __name__ == '__main__':
382 main()
383 main()
General Comments 0
You need to be logged in to leave comments. Login now