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