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