##// END OF EJS Templates
Merge pull request #5339 from minrk/iptest-qt-js-wtf...
Min RK -
r15975:38d326e4 merge
parent child Browse files
Show More
@@ -1,524 +1,524 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 from io import BytesIO
31 from io import BytesIO
32 import os
32 import os
33 import os.path as path
33 import os.path as path
34 import sys
34 import sys
35 from threading import Thread, Lock, Event
35 from threading import Thread, Lock, Event
36 import warnings
36 import warnings
37
37
38 # Now, proceed to import nose itself
38 # Now, proceed to import nose itself
39 import nose.plugins.builtin
39 import nose.plugins.builtin
40 from nose.plugins.xunit import Xunit
40 from nose.plugins.xunit import Xunit
41 from nose import SkipTest
41 from nose import SkipTest
42 from nose.core import TestProgram
42 from nose.core import TestProgram
43 from nose.plugins import Plugin
43 from nose.plugins import Plugin
44 from nose.util import safe_str
44 from nose.util import safe_str
45
45
46 # Our own imports
46 # Our own imports
47 from IPython.utils.process import is_cmd_found
47 from IPython.utils.process import is_cmd_found
48 from IPython.utils.importstring import import_item
48 from IPython.utils.importstring import import_item
49 from IPython.testing.plugin.ipdoctest import IPythonDoctest
49 from IPython.testing.plugin.ipdoctest import IPythonDoctest
50 from IPython.external.decorators import KnownFailure, knownfailureif
50 from IPython.external.decorators import KnownFailure, knownfailureif
51
51
52 pjoin = path.join
52 pjoin = path.join
53
53
54
54
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56 # Globals
56 # Globals
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58
58
59
59
60 #-----------------------------------------------------------------------------
60 #-----------------------------------------------------------------------------
61 # Warnings control
61 # Warnings control
62 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
63
63
64 # Twisted generates annoying warnings with Python 2.6, as will do other code
64 # Twisted generates annoying warnings with Python 2.6, as will do other code
65 # that imports 'sets' as of today
65 # that imports 'sets' as of today
66 warnings.filterwarnings('ignore', 'the sets module is deprecated',
66 warnings.filterwarnings('ignore', 'the sets module is deprecated',
67 DeprecationWarning )
67 DeprecationWarning )
68
68
69 # This one also comes from Twisted
69 # This one also comes from Twisted
70 warnings.filterwarnings('ignore', 'the sha module is deprecated',
70 warnings.filterwarnings('ignore', 'the sha module is deprecated',
71 DeprecationWarning)
71 DeprecationWarning)
72
72
73 # Wx on Fedora11 spits these out
73 # Wx on Fedora11 spits these out
74 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
74 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
75 UserWarning)
75 UserWarning)
76
76
77 # ------------------------------------------------------------------------------
77 # ------------------------------------------------------------------------------
78 # Monkeypatch Xunit to count known failures as skipped.
78 # Monkeypatch Xunit to count known failures as skipped.
79 # ------------------------------------------------------------------------------
79 # ------------------------------------------------------------------------------
80 def monkeypatch_xunit():
80 def monkeypatch_xunit():
81 try:
81 try:
82 knownfailureif(True)(lambda: None)()
82 knownfailureif(True)(lambda: None)()
83 except Exception as e:
83 except Exception as e:
84 KnownFailureTest = type(e)
84 KnownFailureTest = type(e)
85
85
86 def addError(self, test, err, capt=None):
86 def addError(self, test, err, capt=None):
87 if issubclass(err[0], KnownFailureTest):
87 if issubclass(err[0], KnownFailureTest):
88 err = (SkipTest,) + err[1:]
88 err = (SkipTest,) + err[1:]
89 return self.orig_addError(test, err, capt)
89 return self.orig_addError(test, err, capt)
90
90
91 Xunit.orig_addError = Xunit.addError
91 Xunit.orig_addError = Xunit.addError
92 Xunit.addError = addError
92 Xunit.addError = addError
93
93
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95 # Check which dependencies are installed and greater than minimum version.
95 # Check which dependencies are installed and greater than minimum version.
96 #-----------------------------------------------------------------------------
96 #-----------------------------------------------------------------------------
97 def extract_version(mod):
97 def extract_version(mod):
98 return mod.__version__
98 return mod.__version__
99
99
100 def test_for(item, min_version=None, callback=extract_version):
100 def test_for(item, min_version=None, callback=extract_version):
101 """Test to see if item is importable, and optionally check against a minimum
101 """Test to see if item is importable, and optionally check against a minimum
102 version.
102 version.
103
103
104 If min_version is given, the default behavior is to check against the
104 If min_version is given, the default behavior is to check against the
105 `__version__` attribute of the item, but specifying `callback` allows you to
105 `__version__` attribute of the item, but specifying `callback` allows you to
106 extract the value you are interested in. e.g::
106 extract the value you are interested in. e.g::
107
107
108 In [1]: import sys
108 In [1]: import sys
109
109
110 In [2]: from IPython.testing.iptest import test_for
110 In [2]: from IPython.testing.iptest import test_for
111
111
112 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
112 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
113 Out[3]: True
113 Out[3]: True
114
114
115 """
115 """
116 try:
116 try:
117 check = import_item(item)
117 check = import_item(item)
118 except (ImportError, RuntimeError):
118 except (ImportError, RuntimeError):
119 # GTK reports Runtime error if it can't be initialized even if it's
119 # GTK reports Runtime error if it can't be initialized even if it's
120 # importable.
120 # importable.
121 return False
121 return False
122 else:
122 else:
123 if min_version:
123 if min_version:
124 if callback:
124 if callback:
125 # extra processing step to get version to compare
125 # extra processing step to get version to compare
126 check = callback(check)
126 check = callback(check)
127
127
128 return check >= min_version
128 return check >= min_version
129 else:
129 else:
130 return True
130 return True
131
131
132 # Global dict where we can store information on what we have and what we don't
132 # Global dict where we can store information on what we have and what we don't
133 # have available at test run time
133 # have available at test run time
134 have = {}
134 have = {}
135
135
136 have['curses'] = test_for('_curses')
136 have['curses'] = test_for('_curses')
137 have['matplotlib'] = test_for('matplotlib')
137 have['matplotlib'] = test_for('matplotlib')
138 have['numpy'] = test_for('numpy')
138 have['numpy'] = test_for('numpy')
139 have['pexpect'] = test_for('IPython.external.pexpect')
139 have['pexpect'] = test_for('IPython.external.pexpect')
140 have['pymongo'] = test_for('pymongo')
140 have['pymongo'] = test_for('pymongo')
141 have['pygments'] = test_for('pygments')
141 have['pygments'] = test_for('pygments')
142 have['qt'] = test_for('IPython.external.qt')
142 have['qt'] = test_for('IPython.external.qt')
143 have['rpy2'] = test_for('rpy2')
143 have['rpy2'] = test_for('rpy2')
144 have['sqlite3'] = test_for('sqlite3')
144 have['sqlite3'] = test_for('sqlite3')
145 have['cython'] = test_for('Cython')
145 have['cython'] = test_for('Cython')
146 have['oct2py'] = test_for('oct2py')
146 have['oct2py'] = test_for('oct2py')
147 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
147 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
148 have['jinja2'] = test_for('jinja2')
148 have['jinja2'] = test_for('jinja2')
149 have['requests'] = test_for('requests')
149 have['requests'] = test_for('requests')
150 have['sphinx'] = test_for('sphinx')
150 have['sphinx'] = test_for('sphinx')
151 have['casperjs'] = is_cmd_found('casperjs')
151 have['casperjs'] = is_cmd_found('casperjs')
152
152
153 min_zmq = (2,1,11)
153 min_zmq = (2,1,11)
154
154
155 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
155 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
156
156
157 #-----------------------------------------------------------------------------
157 #-----------------------------------------------------------------------------
158 # Test suite definitions
158 # Test suite definitions
159 #-----------------------------------------------------------------------------
159 #-----------------------------------------------------------------------------
160
160
161 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
161 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
162 'extensions', 'lib', 'terminal', 'testing', 'utils',
162 'extensions', 'lib', 'terminal', 'testing', 'utils',
163 'nbformat', 'qt', 'html', 'nbconvert'
163 'nbformat', 'qt', 'html', 'nbconvert'
164 ]
164 ]
165
165
166 class TestSection(object):
166 class TestSection(object):
167 def __init__(self, name, includes):
167 def __init__(self, name, includes):
168 self.name = name
168 self.name = name
169 self.includes = includes
169 self.includes = includes
170 self.excludes = []
170 self.excludes = []
171 self.dependencies = []
171 self.dependencies = []
172 self.enabled = True
172 self.enabled = True
173
173
174 def exclude(self, module):
174 def exclude(self, module):
175 if not module.startswith('IPython'):
175 if not module.startswith('IPython'):
176 module = self.includes[0] + "." + module
176 module = self.includes[0] + "." + module
177 self.excludes.append(module.replace('.', os.sep))
177 self.excludes.append(module.replace('.', os.sep))
178
178
179 def requires(self, *packages):
179 def requires(self, *packages):
180 self.dependencies.extend(packages)
180 self.dependencies.extend(packages)
181
181
182 @property
182 @property
183 def will_run(self):
183 def will_run(self):
184 return self.enabled and all(have[p] for p in self.dependencies)
184 return self.enabled and all(have[p] for p in self.dependencies)
185
185
186 # Name -> (include, exclude, dependencies_met)
186 # Name -> (include, exclude, dependencies_met)
187 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
187 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
188
188
189 # Exclusions and dependencies
189 # Exclusions and dependencies
190 # ---------------------------
190 # ---------------------------
191
191
192 # core:
192 # core:
193 sec = test_sections['core']
193 sec = test_sections['core']
194 if not have['sqlite3']:
194 if not have['sqlite3']:
195 sec.exclude('tests.test_history')
195 sec.exclude('tests.test_history')
196 sec.exclude('history')
196 sec.exclude('history')
197 if not have['matplotlib']:
197 if not have['matplotlib']:
198 sec.exclude('pylabtools'),
198 sec.exclude('pylabtools'),
199 sec.exclude('tests.test_pylabtools')
199 sec.exclude('tests.test_pylabtools')
200
200
201 # lib:
201 # lib:
202 sec = test_sections['lib']
202 sec = test_sections['lib']
203 if not have['zmq']:
203 if not have['zmq']:
204 sec.exclude('kernel')
204 sec.exclude('kernel')
205 # We do this unconditionally, so that the test suite doesn't import
205 # We do this unconditionally, so that the test suite doesn't import
206 # gtk, changing the default encoding and masking some unicode bugs.
206 # gtk, changing the default encoding and masking some unicode bugs.
207 sec.exclude('inputhookgtk')
207 sec.exclude('inputhookgtk')
208 # We also do this unconditionally, because wx can interfere with Unix signals.
208 # We also do this unconditionally, because wx can interfere with Unix signals.
209 # There are currently no tests for it anyway.
209 # There are currently no tests for it anyway.
210 sec.exclude('inputhookwx')
210 sec.exclude('inputhookwx')
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['testing']
217 sec = test_sections['testing']
218 # These have to be skipped on win32 because they use echo, rm, cd, etc.
218 # These have to be skipped on win32 because they use echo, rm, cd, etc.
219 # See ticket https://github.com/ipython/ipython/issues/87
219 # See ticket https://github.com/ipython/ipython/issues/87
220 if sys.platform == 'win32':
220 if sys.platform == 'win32':
221 sec.exclude('plugin.test_exampleip')
221 sec.exclude('plugin.test_exampleip')
222 sec.exclude('plugin.dtexample')
222 sec.exclude('plugin.dtexample')
223
223
224 # terminal:
224 # terminal:
225 if (not have['pexpect']) or (not have['zmq']):
225 if (not have['pexpect']) or (not have['zmq']):
226 test_sections['terminal'].exclude('console')
226 test_sections['terminal'].exclude('console')
227
227
228 # parallel
228 # parallel
229 sec = test_sections['parallel']
229 sec = test_sections['parallel']
230 sec.requires('zmq')
230 sec.requires('zmq')
231 if not have['pymongo']:
231 if not have['pymongo']:
232 sec.exclude('controller.mongodb')
232 sec.exclude('controller.mongodb')
233 sec.exclude('tests.test_mongodb')
233 sec.exclude('tests.test_mongodb')
234
234
235 # kernel:
235 # kernel:
236 sec = test_sections['kernel']
236 sec = test_sections['kernel']
237 sec.requires('zmq')
237 sec.requires('zmq')
238 # The in-process kernel tests are done in a separate section
238 # The in-process kernel tests are done in a separate section
239 sec.exclude('inprocess')
239 sec.exclude('inprocess')
240 # importing gtk sets the default encoding, which we want to avoid
240 # importing gtk sets the default encoding, which we want to avoid
241 sec.exclude('zmq.gui.gtkembed')
241 sec.exclude('zmq.gui.gtkembed')
242 if not have['matplotlib']:
242 if not have['matplotlib']:
243 sec.exclude('zmq.pylab')
243 sec.exclude('zmq.pylab')
244
244
245 # kernel.inprocess:
245 # kernel.inprocess:
246 test_sections['kernel.inprocess'].requires('zmq')
246 test_sections['kernel.inprocess'].requires('zmq')
247
247
248 # extensions:
248 # extensions:
249 sec = test_sections['extensions']
249 sec = test_sections['extensions']
250 if not have['cython']:
250 if not have['cython']:
251 sec.exclude('cythonmagic')
251 sec.exclude('cythonmagic')
252 sec.exclude('tests.test_cythonmagic')
252 sec.exclude('tests.test_cythonmagic')
253 if not have['oct2py']:
253 if not have['oct2py']:
254 sec.exclude('octavemagic')
254 sec.exclude('octavemagic')
255 sec.exclude('tests.test_octavemagic')
255 sec.exclude('tests.test_octavemagic')
256 if not have['rpy2'] or not have['numpy']:
256 if not have['rpy2'] or not have['numpy']:
257 sec.exclude('rmagic')
257 sec.exclude('rmagic')
258 sec.exclude('tests.test_rmagic')
258 sec.exclude('tests.test_rmagic')
259 # autoreload does some strange stuff, so move it to its own test section
259 # autoreload does some strange stuff, so move it to its own test section
260 sec.exclude('autoreload')
260 sec.exclude('autoreload')
261 sec.exclude('tests.test_autoreload')
261 sec.exclude('tests.test_autoreload')
262 test_sections['autoreload'] = TestSection('autoreload',
262 test_sections['autoreload'] = TestSection('autoreload',
263 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
263 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
264 test_group_names.append('autoreload')
264 test_group_names.append('autoreload')
265
265
266 # qt:
266 # qt:
267 test_sections['qt'].requires('zmq', 'qt', 'pygments')
267 test_sections['qt'].requires('zmq', 'qt', 'pygments')
268
268
269 # html:
269 # html:
270 sec = test_sections['html']
270 sec = test_sections['html']
271 sec.requires('zmq', 'tornado', 'requests')
271 sec.requires('zmq', 'tornado', 'requests', 'sqlite3')
272 # The notebook 'static' directory contains JS, css and other
272 # The notebook 'static' directory contains JS, css and other
273 # files for web serving. Occasionally projects may put a .py
273 # files for web serving. Occasionally projects may put a .py
274 # file in there (MathJax ships a conf.py), so we might as
274 # file in there (MathJax ships a conf.py), so we might as
275 # well play it safe and skip the whole thing.
275 # well play it safe and skip the whole thing.
276 sec.exclude('static')
276 sec.exclude('static')
277 sec.exclude('fabfile')
277 sec.exclude('fabfile')
278 if not have['jinja2']:
278 if not have['jinja2']:
279 sec.exclude('notebookapp')
279 sec.exclude('notebookapp')
280 if not have['pygments'] or not have['jinja2']:
280 if not have['pygments'] or not have['jinja2']:
281 sec.exclude('nbconvert')
281 sec.exclude('nbconvert')
282
282
283 # config:
283 # config:
284 # Config files aren't really importable stand-alone
284 # Config files aren't really importable stand-alone
285 test_sections['config'].exclude('profile')
285 test_sections['config'].exclude('profile')
286
286
287 # nbconvert:
287 # nbconvert:
288 sec = test_sections['nbconvert']
288 sec = test_sections['nbconvert']
289 sec.requires('pygments', 'jinja2')
289 sec.requires('pygments', 'jinja2')
290 # Exclude nbconvert directories containing config files used to test.
290 # Exclude nbconvert directories containing config files used to test.
291 # Executing the config files with iptest would cause an exception.
291 # Executing the config files with iptest would cause an exception.
292 sec.exclude('tests.files')
292 sec.exclude('tests.files')
293 sec.exclude('exporters.tests.files')
293 sec.exclude('exporters.tests.files')
294 if not have['tornado']:
294 if not have['tornado']:
295 sec.exclude('nbconvert.post_processors.serve')
295 sec.exclude('nbconvert.post_processors.serve')
296 sec.exclude('nbconvert.post_processors.tests.test_serve')
296 sec.exclude('nbconvert.post_processors.tests.test_serve')
297
297
298 #-----------------------------------------------------------------------------
298 #-----------------------------------------------------------------------------
299 # Functions and classes
299 # Functions and classes
300 #-----------------------------------------------------------------------------
300 #-----------------------------------------------------------------------------
301
301
302 def check_exclusions_exist():
302 def check_exclusions_exist():
303 from IPython.utils.path import get_ipython_package_dir
303 from IPython.utils.path import get_ipython_package_dir
304 from IPython.utils.warn import warn
304 from IPython.utils.warn import warn
305 parent = os.path.dirname(get_ipython_package_dir())
305 parent = os.path.dirname(get_ipython_package_dir())
306 for sec in test_sections:
306 for sec in test_sections:
307 for pattern in sec.exclusions:
307 for pattern in sec.exclusions:
308 fullpath = pjoin(parent, pattern)
308 fullpath = pjoin(parent, pattern)
309 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
309 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
310 warn("Excluding nonexistent file: %r" % pattern)
310 warn("Excluding nonexistent file: %r" % pattern)
311
311
312
312
313 class ExclusionPlugin(Plugin):
313 class ExclusionPlugin(Plugin):
314 """A nose plugin to effect our exclusions of files and directories.
314 """A nose plugin to effect our exclusions of files and directories.
315 """
315 """
316 name = 'exclusions'
316 name = 'exclusions'
317 score = 3000 # Should come before any other plugins
317 score = 3000 # Should come before any other plugins
318
318
319 def __init__(self, exclude_patterns=None):
319 def __init__(self, exclude_patterns=None):
320 """
320 """
321 Parameters
321 Parameters
322 ----------
322 ----------
323
323
324 exclude_patterns : sequence of strings, optional
324 exclude_patterns : sequence of strings, optional
325 Filenames containing these patterns (as raw strings, not as regular
325 Filenames containing these patterns (as raw strings, not as regular
326 expressions) are excluded from the tests.
326 expressions) are excluded from the tests.
327 """
327 """
328 self.exclude_patterns = exclude_patterns or []
328 self.exclude_patterns = exclude_patterns or []
329 super(ExclusionPlugin, self).__init__()
329 super(ExclusionPlugin, self).__init__()
330
330
331 def options(self, parser, env=os.environ):
331 def options(self, parser, env=os.environ):
332 Plugin.options(self, parser, env)
332 Plugin.options(self, parser, env)
333
333
334 def configure(self, options, config):
334 def configure(self, options, config):
335 Plugin.configure(self, options, config)
335 Plugin.configure(self, options, config)
336 # Override nose trying to disable plugin.
336 # Override nose trying to disable plugin.
337 self.enabled = True
337 self.enabled = True
338
338
339 def wantFile(self, filename):
339 def wantFile(self, filename):
340 """Return whether the given filename should be scanned for tests.
340 """Return whether the given filename should be scanned for tests.
341 """
341 """
342 if any(pat in filename for pat in self.exclude_patterns):
342 if any(pat in filename for pat in self.exclude_patterns):
343 return False
343 return False
344 return None
344 return None
345
345
346 def wantDirectory(self, directory):
346 def wantDirectory(self, directory):
347 """Return whether the given directory should be scanned for tests.
347 """Return whether the given directory should be scanned for tests.
348 """
348 """
349 if any(pat in directory for pat in self.exclude_patterns):
349 if any(pat in directory for pat in self.exclude_patterns):
350 return False
350 return False
351 return None
351 return None
352
352
353
353
354 class StreamCapturer(Thread):
354 class StreamCapturer(Thread):
355 daemon = True # Don't hang if main thread crashes
355 daemon = True # Don't hang if main thread crashes
356 started = False
356 started = False
357 def __init__(self):
357 def __init__(self):
358 super(StreamCapturer, self).__init__()
358 super(StreamCapturer, self).__init__()
359 self.streams = []
359 self.streams = []
360 self.buffer = BytesIO()
360 self.buffer = BytesIO()
361 self.readfd, self.writefd = os.pipe()
361 self.readfd, self.writefd = os.pipe()
362 self.buffer_lock = Lock()
362 self.buffer_lock = Lock()
363 self.stop = Event()
363 self.stop = Event()
364
364
365 def run(self):
365 def run(self):
366 self.started = True
366 self.started = True
367
367
368 while not self.stop.is_set():
368 while not self.stop.is_set():
369 chunk = os.read(self.readfd, 1024)
369 chunk = os.read(self.readfd, 1024)
370
370
371 with self.buffer_lock:
371 with self.buffer_lock:
372 self.buffer.write(chunk)
372 self.buffer.write(chunk)
373
373
374 os.close(self.readfd)
374 os.close(self.readfd)
375 os.close(self.writefd)
375 os.close(self.writefd)
376
376
377 def reset_buffer(self):
377 def reset_buffer(self):
378 with self.buffer_lock:
378 with self.buffer_lock:
379 self.buffer.truncate(0)
379 self.buffer.truncate(0)
380 self.buffer.seek(0)
380 self.buffer.seek(0)
381
381
382 def get_buffer(self):
382 def get_buffer(self):
383 with self.buffer_lock:
383 with self.buffer_lock:
384 return self.buffer.getvalue()
384 return self.buffer.getvalue()
385
385
386 def ensure_started(self):
386 def ensure_started(self):
387 if not self.started:
387 if not self.started:
388 self.start()
388 self.start()
389
389
390 def halt(self):
390 def halt(self):
391 """Safely stop the thread."""
391 """Safely stop the thread."""
392 if not self.started:
392 if not self.started:
393 return
393 return
394
394
395 self.stop.set()
395 self.stop.set()
396 os.write(self.writefd, b'wake up') # Ensure we're not locked in a read()
396 os.write(self.writefd, b'wake up') # Ensure we're not locked in a read()
397 self.join()
397 self.join()
398
398
399 class SubprocessStreamCapturePlugin(Plugin):
399 class SubprocessStreamCapturePlugin(Plugin):
400 name='subprocstreams'
400 name='subprocstreams'
401 def __init__(self):
401 def __init__(self):
402 Plugin.__init__(self)
402 Plugin.__init__(self)
403 self.stream_capturer = StreamCapturer()
403 self.stream_capturer = StreamCapturer()
404 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
404 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
405 # This is ugly, but distant parts of the test machinery need to be able
405 # This is ugly, but distant parts of the test machinery need to be able
406 # to redirect streams, so we make the object globally accessible.
406 # to redirect streams, so we make the object globally accessible.
407 nose.iptest_stdstreams_fileno = self.get_write_fileno
407 nose.iptest_stdstreams_fileno = self.get_write_fileno
408
408
409 def get_write_fileno(self):
409 def get_write_fileno(self):
410 if self.destination == 'capture':
410 if self.destination == 'capture':
411 self.stream_capturer.ensure_started()
411 self.stream_capturer.ensure_started()
412 return self.stream_capturer.writefd
412 return self.stream_capturer.writefd
413 elif self.destination == 'discard':
413 elif self.destination == 'discard':
414 return os.open(os.devnull, os.O_WRONLY)
414 return os.open(os.devnull, os.O_WRONLY)
415 else:
415 else:
416 return sys.__stdout__.fileno()
416 return sys.__stdout__.fileno()
417
417
418 def configure(self, options, config):
418 def configure(self, options, config):
419 Plugin.configure(self, options, config)
419 Plugin.configure(self, options, config)
420 # Override nose trying to disable plugin.
420 # Override nose trying to disable plugin.
421 if self.destination == 'capture':
421 if self.destination == 'capture':
422 self.enabled = True
422 self.enabled = True
423
423
424 def startTest(self, test):
424 def startTest(self, test):
425 # Reset log capture
425 # Reset log capture
426 self.stream_capturer.reset_buffer()
426 self.stream_capturer.reset_buffer()
427
427
428 def formatFailure(self, test, err):
428 def formatFailure(self, test, err):
429 # Show output
429 # Show output
430 ec, ev, tb = err
430 ec, ev, tb = err
431 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
431 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
432 if captured.strip():
432 if captured.strip():
433 ev = safe_str(ev)
433 ev = safe_str(ev)
434 out = [ev, '>> begin captured subprocess output <<',
434 out = [ev, '>> begin captured subprocess output <<',
435 captured,
435 captured,
436 '>> end captured subprocess output <<']
436 '>> end captured subprocess output <<']
437 return ec, '\n'.join(out), tb
437 return ec, '\n'.join(out), tb
438
438
439 return err
439 return err
440
440
441 formatError = formatFailure
441 formatError = formatFailure
442
442
443 def finalize(self, result):
443 def finalize(self, result):
444 self.stream_capturer.halt()
444 self.stream_capturer.halt()
445
445
446
446
447 def run_iptest():
447 def run_iptest():
448 """Run the IPython test suite using nose.
448 """Run the IPython test suite using nose.
449
449
450 This function is called when this script is **not** called with the form
450 This function is called when this script is **not** called with the form
451 `iptest all`. It simply calls nose with appropriate command line flags
451 `iptest all`. It simply calls nose with appropriate command line flags
452 and accepts all of the standard nose arguments.
452 and accepts all of the standard nose arguments.
453 """
453 """
454 # Apply our monkeypatch to Xunit
454 # Apply our monkeypatch to Xunit
455 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
455 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
456 monkeypatch_xunit()
456 monkeypatch_xunit()
457
457
458 warnings.filterwarnings('ignore',
458 warnings.filterwarnings('ignore',
459 'This will be removed soon. Use IPython.testing.util instead')
459 'This will be removed soon. Use IPython.testing.util instead')
460
460
461 arg1 = sys.argv[1]
461 arg1 = sys.argv[1]
462 if arg1 in test_sections:
462 if arg1 in test_sections:
463 section = test_sections[arg1]
463 section = test_sections[arg1]
464 sys.argv[1:2] = section.includes
464 sys.argv[1:2] = section.includes
465 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
465 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
466 section = test_sections[arg1[8:]]
466 section = test_sections[arg1[8:]]
467 sys.argv[1:2] = section.includes
467 sys.argv[1:2] = section.includes
468 else:
468 else:
469 section = TestSection(arg1, includes=[arg1])
469 section = TestSection(arg1, includes=[arg1])
470
470
471
471
472 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
472 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
473
473
474 '--with-ipdoctest',
474 '--with-ipdoctest',
475 '--ipdoctest-tests','--ipdoctest-extension=txt',
475 '--ipdoctest-tests','--ipdoctest-extension=txt',
476
476
477 # We add --exe because of setuptools' imbecility (it
477 # We add --exe because of setuptools' imbecility (it
478 # blindly does chmod +x on ALL files). Nose does the
478 # blindly does chmod +x on ALL files). Nose does the
479 # right thing and it tries to avoid executables,
479 # right thing and it tries to avoid executables,
480 # setuptools unfortunately forces our hand here. This
480 # setuptools unfortunately forces our hand here. This
481 # has been discussed on the distutils list and the
481 # has been discussed on the distutils list and the
482 # setuptools devs refuse to fix this problem!
482 # setuptools devs refuse to fix this problem!
483 '--exe',
483 '--exe',
484 ]
484 ]
485 if '-a' not in argv and '-A' not in argv:
485 if '-a' not in argv and '-A' not in argv:
486 argv = argv + ['-a', '!crash']
486 argv = argv + ['-a', '!crash']
487
487
488 if nose.__version__ >= '0.11':
488 if nose.__version__ >= '0.11':
489 # I don't fully understand why we need this one, but depending on what
489 # I don't fully understand why we need this one, but depending on what
490 # directory the test suite is run from, if we don't give it, 0 tests
490 # directory the test suite is run from, if we don't give it, 0 tests
491 # get run. Specifically, if the test suite is run from the source dir
491 # get run. Specifically, if the test suite is run from the source dir
492 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
492 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
493 # even if the same call done in this directory works fine). It appears
493 # even if the same call done in this directory works fine). It appears
494 # that if the requested package is in the current dir, nose bails early
494 # that if the requested package is in the current dir, nose bails early
495 # by default. Since it's otherwise harmless, leave it in by default
495 # by default. Since it's otherwise harmless, leave it in by default
496 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
496 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
497 argv.append('--traverse-namespace')
497 argv.append('--traverse-namespace')
498
498
499 # use our plugin for doctesting. It will remove the standard doctest plugin
499 # use our plugin for doctesting. It will remove the standard doctest plugin
500 # if it finds it enabled
500 # if it finds it enabled
501 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
501 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
502 SubprocessStreamCapturePlugin() ]
502 SubprocessStreamCapturePlugin() ]
503
503
504 # Use working directory set by parent process (see iptestcontroller)
504 # Use working directory set by parent process (see iptestcontroller)
505 if 'IPTEST_WORKING_DIR' in os.environ:
505 if 'IPTEST_WORKING_DIR' in os.environ:
506 os.chdir(os.environ['IPTEST_WORKING_DIR'])
506 os.chdir(os.environ['IPTEST_WORKING_DIR'])
507
507
508 # We need a global ipython running in this process, but the special
508 # We need a global ipython running in this process, but the special
509 # in-process group spawns its own IPython kernels, so for *that* group we
509 # in-process group spawns its own IPython kernels, so for *that* group we
510 # must avoid also opening the global one (otherwise there's a conflict of
510 # must avoid also opening the global one (otherwise there's a conflict of
511 # singletons). Ultimately the solution to this problem is to refactor our
511 # singletons). Ultimately the solution to this problem is to refactor our
512 # assumptions about what needs to be a singleton and what doesn't (app
512 # assumptions about what needs to be a singleton and what doesn't (app
513 # objects should, individual shells shouldn't). But for now, this
513 # objects should, individual shells shouldn't). But for now, this
514 # workaround allows the test suite for the inprocess module to complete.
514 # workaround allows the test suite for the inprocess module to complete.
515 if 'kernel.inprocess' not in section.name:
515 if 'kernel.inprocess' not in section.name:
516 from IPython.testing import globalipapp
516 from IPython.testing import globalipapp
517 globalipapp.start_ipython()
517 globalipapp.start_ipython()
518
518
519 # Now nose can run
519 # Now nose can run
520 TestProgram(argv=argv, addplugins=plugins)
520 TestProgram(argv=argv, addplugins=plugins)
521
521
522 if __name__ == '__main__':
522 if __name__ == '__main__':
523 run_iptest()
523 run_iptest()
524
524
@@ -1,563 +1,592 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 json
22 import multiprocessing.pool
23 import multiprocessing.pool
23 from multiprocessing import Process, Queue
24 import os
24 import os
25 import shutil
25 import shutil
26 import signal
26 import signal
27 import sys
27 import sys
28 import subprocess
28 import subprocess
29 import time
29 import time
30
30
31 from .iptest import have, test_group_names as py_test_group_names, test_sections
31 from .iptest import have, test_group_names as py_test_group_names, test_sections, StreamCapturer
32 from IPython.utils.path import compress_user
32 from IPython.utils.path import compress_user
33 from IPython.utils.py3compat import bytes_to_str
33 from IPython.utils.py3compat import bytes_to_str
34 from IPython.utils.sysinfo import get_sys_info
34 from IPython.utils.sysinfo import get_sys_info
35 from IPython.utils.tempdir import TemporaryDirectory
35 from IPython.utils.tempdir import TemporaryDirectory
36
36
37
37
38 class TestController(object):
38 class TestController(object):
39 """Run tests in a subprocess
39 """Run tests in a subprocess
40 """
40 """
41 #: str, IPython test suite to be executed.
41 #: str, IPython test suite to be executed.
42 section = None
42 section = None
43 #: list, command line arguments to be executed
43 #: list, command line arguments to be executed
44 cmd = None
44 cmd = None
45 #: dict, extra environment variables to set for the subprocess
45 #: dict, extra environment variables to set for the subprocess
46 env = None
46 env = None
47 #: list, TemporaryDirectory instances to clear up when the process finishes
47 #: list, TemporaryDirectory instances to clear up when the process finishes
48 dirs = None
48 dirs = None
49 #: subprocess.Popen instance
49 #: subprocess.Popen instance
50 process = None
50 process = None
51 #: str, process stdout+stderr
51 #: str, process stdout+stderr
52 stdout = None
52 stdout = None
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 def setup(self):
59 def setup(self):
60 """Create temporary directories etc.
60 """Create temporary directories etc.
61
61
62 This is only called when we know the test group will be run. Things
62 This is only called when we know the test group will be run. Things
63 created here may be cleaned up by self.cleanup().
63 created here may be cleaned up by self.cleanup().
64 """
64 """
65 pass
65 pass
66
66
67 def launch(self, buffer_output=False):
67 def launch(self, buffer_output=False):
68 # print('*** ENV:', self.env) # dbg
68 # print('*** ENV:', self.env) # dbg
69 # print('*** CMD:', self.cmd) # dbg
69 # print('*** CMD:', self.cmd) # dbg
70 env = os.environ.copy()
70 env = os.environ.copy()
71 env.update(self.env)
71 env.update(self.env)
72 output = subprocess.PIPE if buffer_output else None
72 output = subprocess.PIPE if buffer_output else None
73 stdout = subprocess.STDOUT if buffer_output else None
73 stdout = subprocess.STDOUT if buffer_output else None
74 self.process = subprocess.Popen(self.cmd, stdout=output,
74 self.process = subprocess.Popen(self.cmd, stdout=output,
75 stderr=stdout, env=env)
75 stderr=stdout, env=env)
76
76
77 def wait(self):
77 def wait(self):
78 self.stdout, _ = self.process.communicate()
78 self.stdout, _ = self.process.communicate()
79 return self.process.returncode
79 return self.process.returncode
80
80
81 def print_extra_info(self):
81 def print_extra_info(self):
82 """Print extra information about this test run.
82 """Print extra information about this test run.
83
83
84 If we're running in parallel and showing the concise view, this is only
84 If we're running in parallel and showing the concise view, this is only
85 called if the test group fails. Otherwise, it's called before the test
85 called if the test group fails. Otherwise, it's called before the test
86 group is started.
86 group is started.
87
87
88 The base implementation does nothing, but it can be overridden by
88 The base implementation does nothing, but it can be overridden by
89 subclasses.
89 subclasses.
90 """
90 """
91 return
91 return
92
92
93 def cleanup_process(self):
93 def cleanup_process(self):
94 """Cleanup on exit by killing any leftover processes."""
94 """Cleanup on exit by killing any leftover processes."""
95 subp = self.process
95 subp = self.process
96 if subp is None or (subp.poll() is not None):
96 if subp is None or (subp.poll() is not None):
97 return # Process doesn't exist, or is already dead.
97 return # Process doesn't exist, or is already dead.
98
98
99 try:
99 try:
100 print('Cleaning up stale PID: %d' % subp.pid)
100 print('Cleaning up stale PID: %d' % subp.pid)
101 subp.kill()
101 subp.kill()
102 except: # (OSError, WindowsError) ?
102 except: # (OSError, WindowsError) ?
103 # This is just a best effort, if we fail or the process was
103 # This is just a best effort, if we fail or the process was
104 # really gone, ignore it.
104 # really gone, ignore it.
105 pass
105 pass
106 else:
106 else:
107 for i in range(10):
107 for i in range(10):
108 if subp.poll() is None:
108 if subp.poll() is None:
109 time.sleep(0.1)
109 time.sleep(0.1)
110 else:
110 else:
111 break
111 break
112
112
113 if subp.poll() is None:
113 if subp.poll() is None:
114 # The process did not die...
114 # The process did not die...
115 print('... failed. Manual cleanup may be required.')
115 print('... failed. Manual cleanup may be required.')
116
116
117 def cleanup(self):
117 def cleanup(self):
118 "Kill process if it's still alive, and clean up temporary directories"
118 "Kill process if it's still alive, and clean up temporary directories"
119 self.cleanup_process()
119 self.cleanup_process()
120 for td in self.dirs:
120 for td in self.dirs:
121 td.cleanup()
121 td.cleanup()
122
122
123 __del__ = cleanup
123 __del__ = cleanup
124
124
125 class PyTestController(TestController):
125 class PyTestController(TestController):
126 """Run Python tests using IPython.testing.iptest"""
126 """Run Python tests using IPython.testing.iptest"""
127 #: str, Python command to execute in subprocess
127 #: str, Python command to execute in subprocess
128 pycmd = None
128 pycmd = None
129
129
130 def __init__(self, section, options):
130 def __init__(self, section, options):
131 """Create new test runner."""
131 """Create new test runner."""
132 TestController.__init__(self)
132 TestController.__init__(self)
133 self.section = section
133 self.section = section
134 # pycmd is put into cmd[2] in PyTestController.launch()
134 # pycmd is put into cmd[2] in PyTestController.launch()
135 self.cmd = [sys.executable, '-c', None, section]
135 self.cmd = [sys.executable, '-c', None, section]
136 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
136 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
137 self.options = options
137 self.options = options
138
138
139 def setup(self):
139 def setup(self):
140 ipydir = TemporaryDirectory()
140 ipydir = TemporaryDirectory()
141 self.dirs.append(ipydir)
141 self.dirs.append(ipydir)
142 self.env['IPYTHONDIR'] = ipydir.name
142 self.env['IPYTHONDIR'] = ipydir.name
143 self.workingdir = workingdir = TemporaryDirectory()
143 self.workingdir = workingdir = TemporaryDirectory()
144 self.dirs.append(workingdir)
144 self.dirs.append(workingdir)
145 self.env['IPTEST_WORKING_DIR'] = workingdir.name
145 self.env['IPTEST_WORKING_DIR'] = workingdir.name
146 # This means we won't get odd effects from our own matplotlib config
146 # This means we won't get odd effects from our own matplotlib config
147 self.env['MPLCONFIGDIR'] = workingdir.name
147 self.env['MPLCONFIGDIR'] = workingdir.name
148
148
149 # From options:
149 # From options:
150 if self.options.xunit:
150 if self.options.xunit:
151 self.add_xunit()
151 self.add_xunit()
152 if self.options.coverage:
152 if self.options.coverage:
153 self.add_coverage()
153 self.add_coverage()
154 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
154 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
155 self.cmd.extend(self.options.extra_args)
155 self.cmd.extend(self.options.extra_args)
156
156
157 @property
157 @property
158 def will_run(self):
158 def will_run(self):
159 try:
159 try:
160 return test_sections[self.section].will_run
160 return test_sections[self.section].will_run
161 except KeyError:
161 except KeyError:
162 return True
162 return True
163
163
164 def add_xunit(self):
164 def add_xunit(self):
165 xunit_file = os.path.abspath(self.section + '.xunit.xml')
165 xunit_file = os.path.abspath(self.section + '.xunit.xml')
166 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
166 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
167
167
168 def add_coverage(self):
168 def add_coverage(self):
169 try:
169 try:
170 sources = test_sections[self.section].includes
170 sources = test_sections[self.section].includes
171 except KeyError:
171 except KeyError:
172 sources = ['IPython']
172 sources = ['IPython']
173
173
174 coverage_rc = ("[run]\n"
174 coverage_rc = ("[run]\n"
175 "data_file = {data_file}\n"
175 "data_file = {data_file}\n"
176 "source =\n"
176 "source =\n"
177 " {source}\n"
177 " {source}\n"
178 ).format(data_file=os.path.abspath('.coverage.'+self.section),
178 ).format(data_file=os.path.abspath('.coverage.'+self.section),
179 source="\n ".join(sources))
179 source="\n ".join(sources))
180 config_file = os.path.join(self.workingdir.name, '.coveragerc')
180 config_file = os.path.join(self.workingdir.name, '.coveragerc')
181 with open(config_file, 'w') as f:
181 with open(config_file, 'w') as f:
182 f.write(coverage_rc)
182 f.write(coverage_rc)
183
183
184 self.env['COVERAGE_PROCESS_START'] = config_file
184 self.env['COVERAGE_PROCESS_START'] = config_file
185 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
185 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
186
186
187 def launch(self, buffer_output=False):
187 def launch(self, buffer_output=False):
188 self.cmd[2] = self.pycmd
188 self.cmd[2] = self.pycmd
189 super(PyTestController, self).launch(buffer_output=buffer_output)
189 super(PyTestController, self).launch(buffer_output=buffer_output)
190
190
191 js_prefix = 'js/'
191 js_prefix = 'js/'
192
192
193 def get_js_test_dir():
193 def get_js_test_dir():
194 import IPython.html.tests as t
194 import IPython.html.tests as t
195 return os.path.join(os.path.dirname(t.__file__), '')
195 return os.path.join(os.path.dirname(t.__file__), '')
196
196
197 def all_js_groups():
197 def all_js_groups():
198 import glob
198 import glob
199 test_dir = get_js_test_dir()
199 test_dir = get_js_test_dir()
200 all_subdirs = glob.glob(test_dir + '*/')
200 all_subdirs = glob.glob(test_dir + '*/')
201 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
201 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
202
202
203 class JSController(TestController):
203 class JSController(TestController):
204 """Run CasperJS tests """
204 """Run CasperJS tests """
205 def __init__(self, section):
205 def __init__(self, section):
206 """Create new test runner."""
206 """Create new test runner."""
207 TestController.__init__(self)
207 TestController.__init__(self)
208 self.section = section
208 self.section = section
209 js_test_dir = get_js_test_dir()
209 js_test_dir = get_js_test_dir()
210 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
210 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
211 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
211 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
212 self.cmd = ['casperjs', 'test', includes, test_cases]
212 self.cmd = ['casperjs', 'test', includes, test_cases]
213
213
214 def setup(self):
214 def setup(self):
215 self.ipydir = TemporaryDirectory()
215 self.ipydir = TemporaryDirectory()
216 self.nbdir = TemporaryDirectory()
216 self.nbdir = TemporaryDirectory()
217 self.dirs.append(self.ipydir)
217 self.dirs.append(self.ipydir)
218 self.dirs.append(self.nbdir)
218 self.dirs.append(self.nbdir)
219 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
219 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
220 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
220 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
221
221
222 # start the ipython notebook, so we get the port number
222 # start the ipython notebook, so we get the port number
223 self.server_port = 0
223 self._init_server()
224 self._init_server()
224 self.cmd.append('--port=%s' % self.server_port)
225 if self.server_port:
226 self.cmd.append("--port=%i" % self.server_port)
227 else:
228 # don't launch tests if the server didn't start
229 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
225
230
226 def print_extra_info(self):
231 def print_extra_info(self):
227 print("Running tests with notebook directory %r" % self.nbdir.name)
232 print("Running tests with notebook directory %r" % self.nbdir.name)
228
233
229 @property
234 @property
230 def will_run(self):
235 def will_run(self):
231 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
236 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3'])
232
237
233 def _init_server(self):
238 def _init_server(self):
234 "Start the notebook server in a separate process"
239 "Start the notebook server in a separate process"
235 self.queue = q = Queue()
240 self.server_command = command = [sys.executable,
236 self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name))
241 '-m', 'IPython.html',
237 self.server.start()
242 '--no-browser',
238 self.server_port = q.get()
243 '--ipython-dir', self.ipydir.name,
244 '--notebook-dir', self.nbdir.name,
245 ]
246 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
247 # which run afoul of ipc's maximum path length.
248 if sys.platform.startswith('linux'):
249 command.append('--KernelManager.transport=ipc')
250 self.stream_capturer = c = StreamCapturer()
251 c.start()
252 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT)
253 self.server_info_file = os.path.join(self.ipydir.name,
254 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
255 )
256 self._wait_for_server()
257
258 def _wait_for_server(self):
259 """Wait 30 seconds for the notebook server to start"""
260 for i in range(300):
261 if self.server.poll() is not None:
262 return self._failed_to_start()
263 if os.path.exists(self.server_info_file):
264 self._load_server_info()
265 return
266 time.sleep(0.1)
267 print("Notebook server-info file never arrived: %s" % self.server_info_file,
268 file=sys.stderr
269 )
270
271 def _failed_to_start(self):
272 """Notebook server exited prematurely"""
273 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
274 print("Notebook failed to start: ", file=sys.stderr)
275 print(self.server_command)
276 print(captured, file=sys.stderr)
277
278 def _load_server_info(self):
279 """Notebook server started, load connection info from JSON"""
280 with open(self.server_info_file) as f:
281 info = json.load(f)
282 self.server_port = info['port']
239
283
240 def cleanup(self):
284 def cleanup(self):
285 try:
241 self.server.terminate()
286 self.server.terminate()
242 self.server.join()
287 except OSError:
288 # already dead
289 pass
290 self.server.wait()
291 self.stream_capturer.halt()
243 TestController.cleanup(self)
292 TestController.cleanup(self)
244
293
245 def run_webapp(q, ipydir, nbdir, loglevel=0):
246 """start the IPython Notebook, and pass port back to the queue"""
247 import os
248 import IPython.html.notebookapp as nbapp
249 import sys
250 sys.stderr = open(os.devnull, 'w')
251 server = nbapp.NotebookApp()
252 args = ['--no-browser']
253 args.extend(['--ipython-dir', ipydir,
254 '--notebook-dir', nbdir,
255 '--log-level', str(loglevel),
256 ])
257 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
258 # which run afoul of ipc's maximum path length.
259 if sys.platform.startswith('linux'):
260 args.append('--KernelManager.transport=ipc')
261 server.initialize(args)
262 # communicate the port number to the parent process
263 q.put(server.port)
264 server.start()
265
294
266 def prepare_controllers(options):
295 def prepare_controllers(options):
267 """Returns two lists of TestController instances, those to run, and those
296 """Returns two lists of TestController instances, those to run, and those
268 not to run."""
297 not to run."""
269 testgroups = options.testgroups
298 testgroups = options.testgroups
270
299
271 if testgroups:
300 if testgroups:
272 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
301 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
273 or g.startswith('IPython.')]
302 or g.startswith('IPython.')]
274 if 'js' in testgroups:
303 if 'js' in testgroups:
275 js_testgroups = all_js_groups()
304 js_testgroups = all_js_groups()
276 else:
305 else:
277 js_testgroups = [g for g in testgroups if g not in py_testgroups]
306 js_testgroups = [g for g in testgroups if g not in py_testgroups]
278 else:
307 else:
279 py_testgroups = py_test_group_names
308 py_testgroups = py_test_group_names
280 js_testgroups = all_js_groups()
309 js_testgroups = all_js_groups()
281 if not options.all:
310 if not options.all:
282 test_sections['parallel'].enabled = False
311 test_sections['parallel'].enabled = False
283
312
284 c_js = [JSController(name) for name in js_testgroups]
313 c_js = [JSController(name) for name in js_testgroups]
285 c_py = [PyTestController(name, options) for name in py_testgroups]
314 c_py = [PyTestController(name, options) for name in py_testgroups]
286
315
287 controllers = c_py + c_js
316 controllers = c_py + c_js
288 to_run = [c for c in controllers if c.will_run]
317 to_run = [c for c in controllers if c.will_run]
289 not_run = [c for c in controllers if not c.will_run]
318 not_run = [c for c in controllers if not c.will_run]
290 return to_run, not_run
319 return to_run, not_run
291
320
292 def do_run(controller, buffer_output=True):
321 def do_run(controller, buffer_output=True):
293 """Setup and run a test controller.
322 """Setup and run a test controller.
294
323
295 If buffer_output is True, no output is displayed, to avoid it appearing
324 If buffer_output is True, no output is displayed, to avoid it appearing
296 interleaved. In this case, the caller is responsible for displaying test
325 interleaved. In this case, the caller is responsible for displaying test
297 output on failure.
326 output on failure.
298
327
299 Returns
328 Returns
300 -------
329 -------
301 controller : TestController
330 controller : TestController
302 The same controller as passed in, as a convenience for using map() type
331 The same controller as passed in, as a convenience for using map() type
303 APIs.
332 APIs.
304 exitcode : int
333 exitcode : int
305 The exit code of the test subprocess. Non-zero indicates failure.
334 The exit code of the test subprocess. Non-zero indicates failure.
306 """
335 """
307 try:
336 try:
308 try:
337 try:
309 controller.setup()
338 controller.setup()
310 if not buffer_output:
339 if not buffer_output:
311 controller.print_extra_info()
340 controller.print_extra_info()
312 controller.launch(buffer_output=buffer_output)
341 controller.launch(buffer_output=buffer_output)
313 except Exception:
342 except Exception:
314 import traceback
343 import traceback
315 traceback.print_exc()
344 traceback.print_exc()
316 return controller, 1 # signal failure
345 return controller, 1 # signal failure
317
346
318 exitcode = controller.wait()
347 exitcode = controller.wait()
319 return controller, exitcode
348 return controller, exitcode
320
349
321 except KeyboardInterrupt:
350 except KeyboardInterrupt:
322 return controller, -signal.SIGINT
351 return controller, -signal.SIGINT
323 finally:
352 finally:
324 controller.cleanup()
353 controller.cleanup()
325
354
326 def report():
355 def report():
327 """Return a string with a summary report of test-related variables."""
356 """Return a string with a summary report of test-related variables."""
328 inf = get_sys_info()
357 inf = get_sys_info()
329 out = []
358 out = []
330 def _add(name, value):
359 def _add(name, value):
331 out.append((name, value))
360 out.append((name, value))
332
361
333 _add('IPython version', inf['ipython_version'])
362 _add('IPython version', inf['ipython_version'])
334 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
363 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
335 _add('IPython package', compress_user(inf['ipython_path']))
364 _add('IPython package', compress_user(inf['ipython_path']))
336 _add('Python version', inf['sys_version'].replace('\n',''))
365 _add('Python version', inf['sys_version'].replace('\n',''))
337 _add('sys.executable', compress_user(inf['sys_executable']))
366 _add('sys.executable', compress_user(inf['sys_executable']))
338 _add('Platform', inf['platform'])
367 _add('Platform', inf['platform'])
339
368
340 width = max(len(n) for (n,v) in out)
369 width = max(len(n) for (n,v) in out)
341 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
370 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
342
371
343 avail = []
372 avail = []
344 not_avail = []
373 not_avail = []
345
374
346 for k, is_avail in have.items():
375 for k, is_avail in have.items():
347 if is_avail:
376 if is_avail:
348 avail.append(k)
377 avail.append(k)
349 else:
378 else:
350 not_avail.append(k)
379 not_avail.append(k)
351
380
352 if avail:
381 if avail:
353 out.append('\nTools and libraries available at test time:\n')
382 out.append('\nTools and libraries available at test time:\n')
354 avail.sort()
383 avail.sort()
355 out.append(' ' + ' '.join(avail)+'\n')
384 out.append(' ' + ' '.join(avail)+'\n')
356
385
357 if not_avail:
386 if not_avail:
358 out.append('\nTools and libraries NOT available at test time:\n')
387 out.append('\nTools and libraries NOT available at test time:\n')
359 not_avail.sort()
388 not_avail.sort()
360 out.append(' ' + ' '.join(not_avail)+'\n')
389 out.append(' ' + ' '.join(not_avail)+'\n')
361
390
362 return ''.join(out)
391 return ''.join(out)
363
392
364 def run_iptestall(options):
393 def run_iptestall(options):
365 """Run the entire IPython test suite by calling nose and trial.
394 """Run the entire IPython test suite by calling nose and trial.
366
395
367 This function constructs :class:`IPTester` instances for all IPython
396 This function constructs :class:`IPTester` instances for all IPython
368 modules and package and then runs each of them. This causes the modules
397 modules and package and then runs each of them. This causes the modules
369 and packages of IPython to be tested each in their own subprocess using
398 and packages of IPython to be tested each in their own subprocess using
370 nose.
399 nose.
371
400
372 Parameters
401 Parameters
373 ----------
402 ----------
374
403
375 All parameters are passed as attributes of the options object.
404 All parameters are passed as attributes of the options object.
376
405
377 testgroups : list of str
406 testgroups : list of str
378 Run only these sections of the test suite. If empty, run all the available
407 Run only these sections of the test suite. If empty, run all the available
379 sections.
408 sections.
380
409
381 fast : int or None
410 fast : int or None
382 Run the test suite in parallel, using n simultaneous processes. If None
411 Run the test suite in parallel, using n simultaneous processes. If None
383 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
412 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
384
413
385 inc_slow : bool
414 inc_slow : bool
386 Include slow tests, like IPython.parallel. By default, these tests aren't
415 Include slow tests, like IPython.parallel. By default, these tests aren't
387 run.
416 run.
388
417
389 xunit : bool
418 xunit : bool
390 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
419 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
391
420
392 coverage : bool or str
421 coverage : bool or str
393 Measure code coverage from tests. True will store the raw coverage data,
422 Measure code coverage from tests. True will store the raw coverage data,
394 or pass 'html' or 'xml' to get reports.
423 or pass 'html' or 'xml' to get reports.
395
424
396 extra_args : list
425 extra_args : list
397 Extra arguments to pass to the test subprocesses, e.g. '-v'
426 Extra arguments to pass to the test subprocesses, e.g. '-v'
398 """
427 """
399 to_run, not_run = prepare_controllers(options)
428 to_run, not_run = prepare_controllers(options)
400
429
401 def justify(ltext, rtext, width=70, fill='-'):
430 def justify(ltext, rtext, width=70, fill='-'):
402 ltext += ' '
431 ltext += ' '
403 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
432 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
404 return ltext + rtext
433 return ltext + rtext
405
434
406 # Run all test runners, tracking execution time
435 # Run all test runners, tracking execution time
407 failed = []
436 failed = []
408 t_start = time.time()
437 t_start = time.time()
409
438
410 print()
439 print()
411 if options.fast == 1:
440 if options.fast == 1:
412 # This actually means sequential, i.e. with 1 job
441 # This actually means sequential, i.e. with 1 job
413 for controller in to_run:
442 for controller in to_run:
414 print('Test group:', controller.section)
443 print('Test group:', controller.section)
415 sys.stdout.flush() # Show in correct order when output is piped
444 sys.stdout.flush() # Show in correct order when output is piped
416 controller, res = do_run(controller, buffer_output=False)
445 controller, res = do_run(controller, buffer_output=False)
417 if res:
446 if res:
418 failed.append(controller)
447 failed.append(controller)
419 if res == -signal.SIGINT:
448 if res == -signal.SIGINT:
420 print("Interrupted")
449 print("Interrupted")
421 break
450 break
422 print()
451 print()
423
452
424 else:
453 else:
425 # Run tests concurrently
454 # Run tests concurrently
426 try:
455 try:
427 pool = multiprocessing.pool.ThreadPool(options.fast)
456 pool = multiprocessing.pool.ThreadPool(options.fast)
428 for (controller, res) in pool.imap_unordered(do_run, to_run):
457 for (controller, res) in pool.imap_unordered(do_run, to_run):
429 res_string = 'OK' if res == 0 else 'FAILED'
458 res_string = 'OK' if res == 0 else 'FAILED'
430 print(justify('Test group: ' + controller.section, res_string))
459 print(justify('Test group: ' + controller.section, res_string))
431 if res:
460 if res:
432 controller.print_extra_info()
461 controller.print_extra_info()
433 print(bytes_to_str(controller.stdout))
462 print(bytes_to_str(controller.stdout))
434 failed.append(controller)
463 failed.append(controller)
435 if res == -signal.SIGINT:
464 if res == -signal.SIGINT:
436 print("Interrupted")
465 print("Interrupted")
437 break
466 break
438 except KeyboardInterrupt:
467 except KeyboardInterrupt:
439 return
468 return
440
469
441 for controller in not_run:
470 for controller in not_run:
442 print(justify('Test group: ' + controller.section, 'NOT RUN'))
471 print(justify('Test group: ' + controller.section, 'NOT RUN'))
443
472
444 t_end = time.time()
473 t_end = time.time()
445 t_tests = t_end - t_start
474 t_tests = t_end - t_start
446 nrunners = len(to_run)
475 nrunners = len(to_run)
447 nfail = len(failed)
476 nfail = len(failed)
448 # summarize results
477 # summarize results
449 print('_'*70)
478 print('_'*70)
450 print('Test suite completed for system with the following information:')
479 print('Test suite completed for system with the following information:')
451 print(report())
480 print(report())
452 took = "Took %.3fs." % t_tests
481 took = "Took %.3fs." % t_tests
453 print('Status: ', end='')
482 print('Status: ', end='')
454 if not failed:
483 if not failed:
455 print('OK (%d test groups).' % nrunners, took)
484 print('OK (%d test groups).' % nrunners, took)
456 else:
485 else:
457 # If anything went wrong, point out what command to rerun manually to
486 # If anything went wrong, point out what command to rerun manually to
458 # see the actual errors and individual summary
487 # see the actual errors and individual summary
459 failed_sections = [c.section for c in failed]
488 failed_sections = [c.section for c in failed]
460 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
489 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
461 nrunners, ', '.join(failed_sections)), took)
490 nrunners, ', '.join(failed_sections)), took)
462 print()
491 print()
463 print('You may wish to rerun these, with:')
492 print('You may wish to rerun these, with:')
464 print(' iptest', *failed_sections)
493 print(' iptest', *failed_sections)
465 print()
494 print()
466
495
467 if options.coverage:
496 if options.coverage:
468 from coverage import coverage
497 from coverage import coverage
469 cov = coverage(data_file='.coverage')
498 cov = coverage(data_file='.coverage')
470 cov.combine()
499 cov.combine()
471 cov.save()
500 cov.save()
472
501
473 # Coverage HTML report
502 # Coverage HTML report
474 if options.coverage == 'html':
503 if options.coverage == 'html':
475 html_dir = 'ipy_htmlcov'
504 html_dir = 'ipy_htmlcov'
476 shutil.rmtree(html_dir, ignore_errors=True)
505 shutil.rmtree(html_dir, ignore_errors=True)
477 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
506 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
478 sys.stdout.flush()
507 sys.stdout.flush()
479
508
480 # Custom HTML reporter to clean up module names.
509 # Custom HTML reporter to clean up module names.
481 from coverage.html import HtmlReporter
510 from coverage.html import HtmlReporter
482 class CustomHtmlReporter(HtmlReporter):
511 class CustomHtmlReporter(HtmlReporter):
483 def find_code_units(self, morfs):
512 def find_code_units(self, morfs):
484 super(CustomHtmlReporter, self).find_code_units(morfs)
513 super(CustomHtmlReporter, self).find_code_units(morfs)
485 for cu in self.code_units:
514 for cu in self.code_units:
486 nameparts = cu.name.split(os.sep)
515 nameparts = cu.name.split(os.sep)
487 if 'IPython' not in nameparts:
516 if 'IPython' not in nameparts:
488 continue
517 continue
489 ix = nameparts.index('IPython')
518 ix = nameparts.index('IPython')
490 cu.name = '.'.join(nameparts[ix:])
519 cu.name = '.'.join(nameparts[ix:])
491
520
492 # Reimplement the html_report method with our custom reporter
521 # Reimplement the html_report method with our custom reporter
493 cov._harvest_data()
522 cov._harvest_data()
494 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
523 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
495 html_title='IPython test coverage',
524 html_title='IPython test coverage',
496 )
525 )
497 reporter = CustomHtmlReporter(cov, cov.config)
526 reporter = CustomHtmlReporter(cov, cov.config)
498 reporter.report(None)
527 reporter.report(None)
499 print('done.')
528 print('done.')
500
529
501 # Coverage XML report
530 # Coverage XML report
502 elif options.coverage == 'xml':
531 elif options.coverage == 'xml':
503 cov.xml_report(outfile='ipy_coverage.xml')
532 cov.xml_report(outfile='ipy_coverage.xml')
504
533
505 if failed:
534 if failed:
506 # Ensure that our exit code indicates failure
535 # Ensure that our exit code indicates failure
507 sys.exit(1)
536 sys.exit(1)
508
537
509 argparser = argparse.ArgumentParser(description='Run IPython test suite')
538 argparser = argparse.ArgumentParser(description='Run IPython test suite')
510 argparser.add_argument('testgroups', nargs='*',
539 argparser.add_argument('testgroups', nargs='*',
511 help='Run specified groups of tests. If omitted, run '
540 help='Run specified groups of tests. If omitted, run '
512 'all tests.')
541 'all tests.')
513 argparser.add_argument('--all', action='store_true',
542 argparser.add_argument('--all', action='store_true',
514 help='Include slow tests not run by default.')
543 help='Include slow tests not run by default.')
515 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
544 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
516 help='Run test sections in parallel. This starts as many '
545 help='Run test sections in parallel. This starts as many '
517 'processes as you have cores, or you can specify a number.')
546 'processes as you have cores, or you can specify a number.')
518 argparser.add_argument('--xunit', action='store_true',
547 argparser.add_argument('--xunit', action='store_true',
519 help='Produce Xunit XML results')
548 help='Produce Xunit XML results')
520 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
549 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
521 help="Measure test coverage. Specify 'html' or "
550 help="Measure test coverage. Specify 'html' or "
522 "'xml' to get reports.")
551 "'xml' to get reports.")
523 argparser.add_argument('--subproc-streams', default='capture',
552 argparser.add_argument('--subproc-streams', default='capture',
524 help="What to do with stdout/stderr from subprocesses. "
553 help="What to do with stdout/stderr from subprocesses. "
525 "'capture' (default), 'show' and 'discard' are the options.")
554 "'capture' (default), 'show' and 'discard' are the options.")
526
555
527 def default_options():
556 def default_options():
528 """Get an argparse Namespace object with the default arguments, to pass to
557 """Get an argparse Namespace object with the default arguments, to pass to
529 :func:`run_iptestall`.
558 :func:`run_iptestall`.
530 """
559 """
531 options = argparser.parse_args([])
560 options = argparser.parse_args([])
532 options.extra_args = []
561 options.extra_args = []
533 return options
562 return options
534
563
535 def main():
564 def main():
536 # iptest doesn't work correctly if the working directory is the
565 # iptest doesn't work correctly if the working directory is the
537 # root of the IPython source tree. Tell the user to avoid
566 # root of the IPython source tree. Tell the user to avoid
538 # frustration.
567 # frustration.
539 if os.path.exists(os.path.join(os.getcwd(),
568 if os.path.exists(os.path.join(os.getcwd(),
540 'IPython', 'testing', '__main__.py')):
569 'IPython', 'testing', '__main__.py')):
541 print("Don't run iptest from the IPython source directory",
570 print("Don't run iptest from the IPython source directory",
542 file=sys.stderr)
571 file=sys.stderr)
543 sys.exit(1)
572 sys.exit(1)
544 # Arguments after -- should be passed through to nose. Argparse treats
573 # Arguments after -- should be passed through to nose. Argparse treats
545 # everything after -- as regular positional arguments, so we separate them
574 # everything after -- as regular positional arguments, so we separate them
546 # first.
575 # first.
547 try:
576 try:
548 ix = sys.argv.index('--')
577 ix = sys.argv.index('--')
549 except ValueError:
578 except ValueError:
550 to_parse = sys.argv[1:]
579 to_parse = sys.argv[1:]
551 extra_args = []
580 extra_args = []
552 else:
581 else:
553 to_parse = sys.argv[1:ix]
582 to_parse = sys.argv[1:ix]
554 extra_args = sys.argv[ix+1:]
583 extra_args = sys.argv[ix+1:]
555
584
556 options = argparser.parse_args(to_parse)
585 options = argparser.parse_args(to_parse)
557 options.extra_args = extra_args
586 options.extra_args = extra_args
558
587
559 run_iptestall(options)
588 run_iptestall(options)
560
589
561
590
562 if __name__ == '__main__':
591 if __name__ == '__main__':
563 main()
592 main()
General Comments 0
You need to be logged in to leave comments. Login now