##// END OF EJS Templates
remove ipython_parallel
Min RK -
Show More

The requested changes are too big and content was truncated. Show full diff

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