##// END OF EJS Templates
StreamCapturer should die if main process crashes.
Thomas Kluyver -
Show More
@@ -1,519 +1,520 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Suite Runner.
2 """IPython Test Suite Runner.
3
3
4 This module provides a main entry point to a user script to test IPython
4 This module provides a main entry point to a user script to test IPython
5 itself from the command line. There are two ways of running this script:
5 itself from the command line. There are two ways of running this script:
6
6
7 1. With the syntax `iptest all`. This runs our entire test suite by
7 1. With the syntax `iptest all`. This runs our entire test suite by
8 calling this script (with different arguments) recursively. This
8 calling this script (with different arguments) recursively. This
9 causes modules and package to be tested in different processes, using nose
9 causes modules and package to be tested in different processes, using nose
10 or trial where appropriate.
10 or trial where appropriate.
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 the script simply calls nose, but with special command line flags and
12 the script simply calls nose, but with special command line flags and
13 plugins loaded.
13 plugins loaded.
14
14
15 """
15 """
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Copyright (C) 2009-2011 The IPython Development Team
18 # Copyright (C) 2009-2011 The IPython Development Team
19 #
19 #
20 # Distributed under the terms of the BSD License. The full license is in
20 # Distributed under the terms of the BSD License. The full license is in
21 # the file COPYING, distributed as part of this software.
21 # the file COPYING, distributed as part of this software.
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Imports
25 # Imports
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 from __future__ import print_function
27 from __future__ import print_function
28
28
29 # Stdlib
29 # Stdlib
30 import glob
30 import glob
31 from io import BytesIO
31 from io import BytesIO
32 import os
32 import os
33 import os.path as path
33 import os.path as path
34 import sys
34 import sys
35 from threading import Thread, Lock, Event
35 from threading import Thread, Lock, Event
36 import warnings
36 import warnings
37
37
38 # Now, proceed to import nose itself
38 # Now, proceed to import nose itself
39 import nose.plugins.builtin
39 import nose.plugins.builtin
40 from nose.plugins.xunit import Xunit
40 from nose.plugins.xunit import Xunit
41 from nose import SkipTest
41 from nose import SkipTest
42 from nose.core import TestProgram
42 from nose.core import TestProgram
43 from nose.plugins import Plugin
43 from nose.plugins import Plugin
44 from nose.util import safe_str
44 from nose.util import safe_str
45
45
46 # Our own imports
46 # Our own imports
47 from IPython.utils.process import is_cmd_found
47 from IPython.utils.process import is_cmd_found
48 from IPython.utils.importstring import import_item
48 from IPython.utils.importstring import import_item
49 from IPython.testing.plugin.ipdoctest import IPythonDoctest
49 from IPython.testing.plugin.ipdoctest import IPythonDoctest
50 from IPython.external.decorators import KnownFailure, knownfailureif
50 from IPython.external.decorators import KnownFailure, knownfailureif
51
51
52 pjoin = path.join
52 pjoin = path.join
53
53
54
54
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56 # Globals
56 # Globals
57 #-----------------------------------------------------------------------------
57 #-----------------------------------------------------------------------------
58
58
59
59
60 #-----------------------------------------------------------------------------
60 #-----------------------------------------------------------------------------
61 # Warnings control
61 # Warnings control
62 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
63
63
64 # Twisted generates annoying warnings with Python 2.6, as will do other code
64 # Twisted generates annoying warnings with Python 2.6, as will do other code
65 # that imports 'sets' as of today
65 # that imports 'sets' as of today
66 warnings.filterwarnings('ignore', 'the sets module is deprecated',
66 warnings.filterwarnings('ignore', 'the sets module is deprecated',
67 DeprecationWarning )
67 DeprecationWarning )
68
68
69 # This one also comes from Twisted
69 # This one also comes from Twisted
70 warnings.filterwarnings('ignore', 'the sha module is deprecated',
70 warnings.filterwarnings('ignore', 'the sha module is deprecated',
71 DeprecationWarning)
71 DeprecationWarning)
72
72
73 # Wx on Fedora11 spits these out
73 # Wx on Fedora11 spits these out
74 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
74 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
75 UserWarning)
75 UserWarning)
76
76
77 # ------------------------------------------------------------------------------
77 # ------------------------------------------------------------------------------
78 # Monkeypatch Xunit to count known failures as skipped.
78 # Monkeypatch Xunit to count known failures as skipped.
79 # ------------------------------------------------------------------------------
79 # ------------------------------------------------------------------------------
80 def monkeypatch_xunit():
80 def monkeypatch_xunit():
81 try:
81 try:
82 knownfailureif(True)(lambda: None)()
82 knownfailureif(True)(lambda: None)()
83 except Exception as e:
83 except Exception as e:
84 KnownFailureTest = type(e)
84 KnownFailureTest = type(e)
85
85
86 def addError(self, test, err, capt=None):
86 def addError(self, test, err, capt=None):
87 if issubclass(err[0], KnownFailureTest):
87 if issubclass(err[0], KnownFailureTest):
88 err = (SkipTest,) + err[1:]
88 err = (SkipTest,) + err[1:]
89 return self.orig_addError(test, err, capt)
89 return self.orig_addError(test, err, capt)
90
90
91 Xunit.orig_addError = Xunit.addError
91 Xunit.orig_addError = Xunit.addError
92 Xunit.addError = addError
92 Xunit.addError = addError
93
93
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95 # Check which dependencies are installed and greater than minimum version.
95 # Check which dependencies are installed and greater than minimum version.
96 #-----------------------------------------------------------------------------
96 #-----------------------------------------------------------------------------
97 def extract_version(mod):
97 def extract_version(mod):
98 return mod.__version__
98 return mod.__version__
99
99
100 def test_for(item, min_version=None, callback=extract_version):
100 def test_for(item, min_version=None, callback=extract_version):
101 """Test to see if item is importable, and optionally check against a minimum
101 """Test to see if item is importable, and optionally check against a minimum
102 version.
102 version.
103
103
104 If min_version is given, the default behavior is to check against the
104 If min_version is given, the default behavior is to check against the
105 `__version__` attribute of the item, but specifying `callback` allows you to
105 `__version__` attribute of the item, but specifying `callback` allows you to
106 extract the value you are interested in. e.g::
106 extract the value you are interested in. e.g::
107
107
108 In [1]: import sys
108 In [1]: import sys
109
109
110 In [2]: from IPython.testing.iptest import test_for
110 In [2]: from IPython.testing.iptest import test_for
111
111
112 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
112 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
113 Out[3]: True
113 Out[3]: True
114
114
115 """
115 """
116 try:
116 try:
117 check = import_item(item)
117 check = import_item(item)
118 except (ImportError, RuntimeError):
118 except (ImportError, RuntimeError):
119 # GTK reports Runtime error if it can't be initialized even if it's
119 # GTK reports Runtime error if it can't be initialized even if it's
120 # importable.
120 # importable.
121 return False
121 return False
122 else:
122 else:
123 if min_version:
123 if min_version:
124 if callback:
124 if callback:
125 # extra processing step to get version to compare
125 # extra processing step to get version to compare
126 check = callback(check)
126 check = callback(check)
127
127
128 return check >= min_version
128 return check >= min_version
129 else:
129 else:
130 return True
130 return True
131
131
132 # Global dict where we can store information on what we have and what we don't
132 # Global dict where we can store information on what we have and what we don't
133 # have available at test run time
133 # have available at test run time
134 have = {}
134 have = {}
135
135
136 have['curses'] = test_for('_curses')
136 have['curses'] = test_for('_curses')
137 have['matplotlib'] = test_for('matplotlib')
137 have['matplotlib'] = test_for('matplotlib')
138 have['numpy'] = test_for('numpy')
138 have['numpy'] = test_for('numpy')
139 have['pexpect'] = test_for('IPython.external.pexpect')
139 have['pexpect'] = test_for('IPython.external.pexpect')
140 have['pymongo'] = test_for('pymongo')
140 have['pymongo'] = test_for('pymongo')
141 have['pygments'] = test_for('pygments')
141 have['pygments'] = test_for('pygments')
142 have['qt'] = test_for('IPython.external.qt')
142 have['qt'] = test_for('IPython.external.qt')
143 have['rpy2'] = test_for('rpy2')
143 have['rpy2'] = test_for('rpy2')
144 have['sqlite3'] = test_for('sqlite3')
144 have['sqlite3'] = test_for('sqlite3')
145 have['cython'] = test_for('Cython')
145 have['cython'] = test_for('Cython')
146 have['oct2py'] = test_for('oct2py')
146 have['oct2py'] = test_for('oct2py')
147 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
147 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
148 have['jinja2'] = test_for('jinja2')
148 have['jinja2'] = test_for('jinja2')
149 have['wx'] = test_for('wx')
149 have['wx'] = test_for('wx')
150 have['wx.aui'] = test_for('wx.aui')
150 have['wx.aui'] = test_for('wx.aui')
151 have['azure'] = test_for('azure')
151 have['azure'] = test_for('azure')
152 have['requests'] = test_for('requests')
152 have['requests'] = test_for('requests')
153 have['sphinx'] = test_for('sphinx')
153 have['sphinx'] = test_for('sphinx')
154 have['casperjs'] = is_cmd_found('casperjs')
154 have['casperjs'] = is_cmd_found('casperjs')
155
155
156 min_zmq = (2,1,11)
156 min_zmq = (2,1,11)
157
157
158 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
158 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
159
159
160 #-----------------------------------------------------------------------------
160 #-----------------------------------------------------------------------------
161 # Test suite definitions
161 # Test suite definitions
162 #-----------------------------------------------------------------------------
162 #-----------------------------------------------------------------------------
163
163
164 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
164 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
165 'extensions', 'lib', 'terminal', 'testing', 'utils',
165 'extensions', 'lib', 'terminal', 'testing', 'utils',
166 'nbformat', 'qt', 'html', 'nbconvert'
166 'nbformat', 'qt', 'html', 'nbconvert'
167 ]
167 ]
168
168
169 class TestSection(object):
169 class TestSection(object):
170 def __init__(self, name, includes):
170 def __init__(self, name, includes):
171 self.name = name
171 self.name = name
172 self.includes = includes
172 self.includes = includes
173 self.excludes = []
173 self.excludes = []
174 self.dependencies = []
174 self.dependencies = []
175 self.enabled = True
175 self.enabled = True
176
176
177 def exclude(self, module):
177 def exclude(self, module):
178 if not module.startswith('IPython'):
178 if not module.startswith('IPython'):
179 module = self.includes[0] + "." + module
179 module = self.includes[0] + "." + module
180 self.excludes.append(module.replace('.', os.sep))
180 self.excludes.append(module.replace('.', os.sep))
181
181
182 def requires(self, *packages):
182 def requires(self, *packages):
183 self.dependencies.extend(packages)
183 self.dependencies.extend(packages)
184
184
185 @property
185 @property
186 def will_run(self):
186 def will_run(self):
187 return self.enabled and all(have[p] for p in self.dependencies)
187 return self.enabled and all(have[p] for p in self.dependencies)
188
188
189 # Name -> (include, exclude, dependencies_met)
189 # Name -> (include, exclude, dependencies_met)
190 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
190 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
191
191
192 # Exclusions and dependencies
192 # Exclusions and dependencies
193 # ---------------------------
193 # ---------------------------
194
194
195 # core:
195 # core:
196 sec = test_sections['core']
196 sec = test_sections['core']
197 if not have['sqlite3']:
197 if not have['sqlite3']:
198 sec.exclude('tests.test_history')
198 sec.exclude('tests.test_history')
199 sec.exclude('history')
199 sec.exclude('history')
200 if not have['matplotlib']:
200 if not have['matplotlib']:
201 sec.exclude('pylabtools'),
201 sec.exclude('pylabtools'),
202 sec.exclude('tests.test_pylabtools')
202 sec.exclude('tests.test_pylabtools')
203
203
204 # lib:
204 # lib:
205 sec = test_sections['lib']
205 sec = test_sections['lib']
206 if not have['wx']:
206 if not have['wx']:
207 sec.exclude('inputhookwx')
207 sec.exclude('inputhookwx')
208 if not have['pexpect']:
208 if not have['pexpect']:
209 sec.exclude('irunner')
209 sec.exclude('irunner')
210 sec.exclude('tests.test_irunner')
210 sec.exclude('tests.test_irunner')
211 if not have['zmq']:
211 if not have['zmq']:
212 sec.exclude('kernel')
212 sec.exclude('kernel')
213 # We do this unconditionally, so that the test suite doesn't import
213 # We do this unconditionally, so that the test suite doesn't import
214 # gtk, changing the default encoding and masking some unicode bugs.
214 # gtk, changing the default encoding and masking some unicode bugs.
215 sec.exclude('inputhookgtk')
215 sec.exclude('inputhookgtk')
216 # Testing inputhook will need a lot of thought, to figure out
216 # Testing inputhook will need a lot of thought, to figure out
217 # how to have tests that don't lock up with the gui event
217 # how to have tests that don't lock up with the gui event
218 # loops in the picture
218 # loops in the picture
219 sec.exclude('inputhook')
219 sec.exclude('inputhook')
220
220
221 # testing:
221 # testing:
222 sec = test_sections['testing']
222 sec = test_sections['testing']
223 # This guy is probably attic material
223 # This guy is probably attic material
224 sec.exclude('mkdoctests')
224 sec.exclude('mkdoctests')
225 # These have to be skipped on win32 because they use echo, rm, cd, etc.
225 # These have to be skipped on win32 because they use echo, rm, cd, etc.
226 # See ticket https://github.com/ipython/ipython/issues/87
226 # See ticket https://github.com/ipython/ipython/issues/87
227 if sys.platform == 'win32':
227 if sys.platform == 'win32':
228 sec.exclude('plugin.test_exampleip')
228 sec.exclude('plugin.test_exampleip')
229 sec.exclude('plugin.dtexample')
229 sec.exclude('plugin.dtexample')
230
230
231 # terminal:
231 # terminal:
232 if (not have['pexpect']) or (not have['zmq']):
232 if (not have['pexpect']) or (not have['zmq']):
233 test_sections['terminal'].exclude('console')
233 test_sections['terminal'].exclude('console')
234
234
235 # parallel
235 # parallel
236 sec = test_sections['parallel']
236 sec = test_sections['parallel']
237 sec.requires('zmq')
237 sec.requires('zmq')
238 if not have['pymongo']:
238 if not have['pymongo']:
239 sec.exclude('controller.mongodb')
239 sec.exclude('controller.mongodb')
240 sec.exclude('tests.test_mongodb')
240 sec.exclude('tests.test_mongodb')
241
241
242 # kernel:
242 # kernel:
243 sec = test_sections['kernel']
243 sec = test_sections['kernel']
244 sec.requires('zmq')
244 sec.requires('zmq')
245 # The in-process kernel tests are done in a separate section
245 # The in-process kernel tests are done in a separate section
246 sec.exclude('inprocess')
246 sec.exclude('inprocess')
247 # importing gtk sets the default encoding, which we want to avoid
247 # importing gtk sets the default encoding, which we want to avoid
248 sec.exclude('zmq.gui.gtkembed')
248 sec.exclude('zmq.gui.gtkembed')
249 if not have['matplotlib']:
249 if not have['matplotlib']:
250 sec.exclude('zmq.pylab')
250 sec.exclude('zmq.pylab')
251
251
252 # kernel.inprocess:
252 # kernel.inprocess:
253 test_sections['kernel.inprocess'].requires('zmq')
253 test_sections['kernel.inprocess'].requires('zmq')
254
254
255 # extensions:
255 # extensions:
256 sec = test_sections['extensions']
256 sec = test_sections['extensions']
257 if not have['cython']:
257 if not have['cython']:
258 sec.exclude('cythonmagic')
258 sec.exclude('cythonmagic')
259 sec.exclude('tests.test_cythonmagic')
259 sec.exclude('tests.test_cythonmagic')
260 if not have['oct2py']:
260 if not have['oct2py']:
261 sec.exclude('octavemagic')
261 sec.exclude('octavemagic')
262 sec.exclude('tests.test_octavemagic')
262 sec.exclude('tests.test_octavemagic')
263 if not have['rpy2'] or not have['numpy']:
263 if not have['rpy2'] or not have['numpy']:
264 sec.exclude('rmagic')
264 sec.exclude('rmagic')
265 sec.exclude('tests.test_rmagic')
265 sec.exclude('tests.test_rmagic')
266 # autoreload does some strange stuff, so move it to its own test section
266 # autoreload does some strange stuff, so move it to its own test section
267 sec.exclude('autoreload')
267 sec.exclude('autoreload')
268 sec.exclude('tests.test_autoreload')
268 sec.exclude('tests.test_autoreload')
269 test_sections['autoreload'] = TestSection('autoreload',
269 test_sections['autoreload'] = TestSection('autoreload',
270 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
270 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
271 test_group_names.append('autoreload')
271 test_group_names.append('autoreload')
272
272
273 # qt:
273 # qt:
274 test_sections['qt'].requires('zmq', 'qt', 'pygments')
274 test_sections['qt'].requires('zmq', 'qt', 'pygments')
275
275
276 # html:
276 # html:
277 sec = test_sections['html']
277 sec = test_sections['html']
278 sec.requires('zmq', 'tornado', 'requests')
278 sec.requires('zmq', 'tornado', 'requests')
279 # The notebook 'static' directory contains JS, css and other
279 # The notebook 'static' directory contains JS, css and other
280 # files for web serving. Occasionally projects may put a .py
280 # files for web serving. Occasionally projects may put a .py
281 # file in there (MathJax ships a conf.py), so we might as
281 # file in there (MathJax ships a conf.py), so we might as
282 # well play it safe and skip the whole thing.
282 # well play it safe and skip the whole thing.
283 sec.exclude('static')
283 sec.exclude('static')
284 sec.exclude('fabfile')
284 sec.exclude('fabfile')
285 if not have['jinja2']:
285 if not have['jinja2']:
286 sec.exclude('notebookapp')
286 sec.exclude('notebookapp')
287 if not have['azure']:
287 if not have['azure']:
288 sec.exclude('services.notebooks.azurenbmanager')
288 sec.exclude('services.notebooks.azurenbmanager')
289
289
290 # config:
290 # config:
291 # Config files aren't really importable stand-alone
291 # Config files aren't really importable stand-alone
292 test_sections['config'].exclude('profile')
292 test_sections['config'].exclude('profile')
293
293
294 # nbconvert:
294 # nbconvert:
295 sec = test_sections['nbconvert']
295 sec = test_sections['nbconvert']
296 sec.requires('pygments', 'jinja2', 'sphinx')
296 sec.requires('pygments', 'jinja2', 'sphinx')
297 # Exclude nbconvert directories containing config files used to test.
297 # Exclude nbconvert directories containing config files used to test.
298 # Executing the config files with iptest would cause an exception.
298 # Executing the config files with iptest would cause an exception.
299 sec.exclude('tests.files')
299 sec.exclude('tests.files')
300 sec.exclude('exporters.tests.files')
300 sec.exclude('exporters.tests.files')
301 if not have['tornado']:
301 if not have['tornado']:
302 sec.exclude('nbconvert.post_processors.serve')
302 sec.exclude('nbconvert.post_processors.serve')
303 sec.exclude('nbconvert.post_processors.tests.test_serve')
303 sec.exclude('nbconvert.post_processors.tests.test_serve')
304
304
305 #-----------------------------------------------------------------------------
305 #-----------------------------------------------------------------------------
306 # Functions and classes
306 # Functions and classes
307 #-----------------------------------------------------------------------------
307 #-----------------------------------------------------------------------------
308
308
309 def check_exclusions_exist():
309 def check_exclusions_exist():
310 from IPython.utils.path import get_ipython_package_dir
310 from IPython.utils.path import get_ipython_package_dir
311 from IPython.utils.warn import warn
311 from IPython.utils.warn import warn
312 parent = os.path.dirname(get_ipython_package_dir())
312 parent = os.path.dirname(get_ipython_package_dir())
313 for sec in test_sections:
313 for sec in test_sections:
314 for pattern in sec.exclusions:
314 for pattern in sec.exclusions:
315 fullpath = pjoin(parent, pattern)
315 fullpath = pjoin(parent, pattern)
316 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
316 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
317 warn("Excluding nonexistent file: %r" % pattern)
317 warn("Excluding nonexistent file: %r" % pattern)
318
318
319
319
320 class ExclusionPlugin(Plugin):
320 class ExclusionPlugin(Plugin):
321 """A nose plugin to effect our exclusions of files and directories.
321 """A nose plugin to effect our exclusions of files and directories.
322 """
322 """
323 name = 'exclusions'
323 name = 'exclusions'
324 score = 3000 # Should come before any other plugins
324 score = 3000 # Should come before any other plugins
325
325
326 def __init__(self, exclude_patterns=None):
326 def __init__(self, exclude_patterns=None):
327 """
327 """
328 Parameters
328 Parameters
329 ----------
329 ----------
330
330
331 exclude_patterns : sequence of strings, optional
331 exclude_patterns : sequence of strings, optional
332 Filenames containing these patterns (as raw strings, not as regular
332 Filenames containing these patterns (as raw strings, not as regular
333 expressions) are excluded from the tests.
333 expressions) are excluded from the tests.
334 """
334 """
335 self.exclude_patterns = exclude_patterns or []
335 self.exclude_patterns = exclude_patterns or []
336 super(ExclusionPlugin, self).__init__()
336 super(ExclusionPlugin, self).__init__()
337
337
338 def options(self, parser, env=os.environ):
338 def options(self, parser, env=os.environ):
339 Plugin.options(self, parser, env)
339 Plugin.options(self, parser, env)
340
340
341 def configure(self, options, config):
341 def configure(self, options, config):
342 Plugin.configure(self, options, config)
342 Plugin.configure(self, options, config)
343 # Override nose trying to disable plugin.
343 # Override nose trying to disable plugin.
344 self.enabled = True
344 self.enabled = True
345
345
346 def wantFile(self, filename):
346 def wantFile(self, filename):
347 """Return whether the given filename should be scanned for tests.
347 """Return whether the given filename should be scanned for tests.
348 """
348 """
349 if any(pat in filename for pat in self.exclude_patterns):
349 if any(pat in filename for pat in self.exclude_patterns):
350 return False
350 return False
351 return None
351 return None
352
352
353 def wantDirectory(self, directory):
353 def wantDirectory(self, directory):
354 """Return whether the given directory should be scanned for tests.
354 """Return whether the given directory should be scanned for tests.
355 """
355 """
356 if any(pat in directory for pat in self.exclude_patterns):
356 if any(pat in directory for pat in self.exclude_patterns):
357 return False
357 return False
358 return None
358 return None
359
359
360
360
361 class StreamCapturer(Thread):
361 class StreamCapturer(Thread):
362 daemon = True # Don't hang if main thread crashes
362 started = False
363 started = False
363 def __init__(self):
364 def __init__(self):
364 super(StreamCapturer, self).__init__()
365 super(StreamCapturer, self).__init__()
365 self.streams = []
366 self.streams = []
366 self.buffer = BytesIO()
367 self.buffer = BytesIO()
367 self.readfd, self.writefd = os.pipe()
368 self.readfd, self.writefd = os.pipe()
368 self.buffer_lock = Lock()
369 self.buffer_lock = Lock()
369 self.stop = Event()
370 self.stop = Event()
370
371
371 def run(self):
372 def run(self):
372 self.started = True
373 self.started = True
373
374
374 while not self.stop.is_set():
375 while not self.stop.is_set():
375 chunk = os.read(self.readfd, 1024)
376 chunk = os.read(self.readfd, 1024)
376
377
377 with self.buffer_lock:
378 with self.buffer_lock:
378 self.buffer.write(chunk)
379 self.buffer.write(chunk)
379
380
380 os.close(self.readfd)
381 os.close(self.readfd)
381 os.close(self.writefd)
382 os.close(self.writefd)
382
383
383 def reset_buffer(self):
384 def reset_buffer(self):
384 with self.buffer_lock:
385 with self.buffer_lock:
385 self.buffer.truncate(0)
386 self.buffer.truncate(0)
386 self.buffer.seek(0)
387 self.buffer.seek(0)
387
388
388 def get_buffer(self):
389 def get_buffer(self):
389 with self.buffer_lock:
390 with self.buffer_lock:
390 return self.buffer.getvalue()
391 return self.buffer.getvalue()
391
392
392 def ensure_started(self):
393 def ensure_started(self):
393 if not self.started:
394 if not self.started:
394 self.start()
395 self.start()
395
396
396 def halt(self):
397 def halt(self):
397 """Safely stop the thread."""
398 """Safely stop the thread."""
398 if not self.started:
399 if not self.started:
399 return
400 return
400
401
401 self.stop.set()
402 self.stop.set()
402 os.write(self.writefd, b'wake up') # Ensure we're not locked in a read()
403 os.write(self.writefd, b'wake up') # Ensure we're not locked in a read()
403 self.join()
404 self.join()
404
405
405 class SubprocessStreamCapturePlugin(Plugin):
406 class SubprocessStreamCapturePlugin(Plugin):
406 name='subprocstreams'
407 name='subprocstreams'
407 def __init__(self):
408 def __init__(self):
408 Plugin.__init__(self)
409 Plugin.__init__(self)
409 self.stream_capturer = StreamCapturer()
410 self.stream_capturer = StreamCapturer()
410 # This is ugly, but distant parts of the test machinery need to be able
411 # This is ugly, but distant parts of the test machinery need to be able
411 # to redirect streams, so we make the object globally accessible.
412 # to redirect streams, so we make the object globally accessible.
412 nose.ipy_stream_capturer = self.stream_capturer
413 nose.ipy_stream_capturer = self.stream_capturer
413
414
414 def configure(self, options, config):
415 def configure(self, options, config):
415 Plugin.configure(self, options, config)
416 Plugin.configure(self, options, config)
416 # Override nose trying to disable plugin.
417 # Override nose trying to disable plugin.
417 self.enabled = True
418 self.enabled = True
418
419
419 def startTest(self, test):
420 def startTest(self, test):
420 # Reset log capture
421 # Reset log capture
421 self.stream_capturer.reset_buffer()
422 self.stream_capturer.reset_buffer()
422
423
423 def formatFailure(self, test, err):
424 def formatFailure(self, test, err):
424 # Show output
425 # Show output
425 ec, ev, tb = err
426 ec, ev, tb = err
426 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
427 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
427 if captured.strip():
428 if captured.strip():
428 ev = safe_str(ev)
429 ev = safe_str(ev)
429 out = [ev, '>> begin captured subprocess output <<',
430 out = [ev, '>> begin captured subprocess output <<',
430 captured,
431 captured,
431 '>> end captured subprocess output <<']
432 '>> end captured subprocess output <<']
432 return ec, '\n'.join(out), tb
433 return ec, '\n'.join(out), tb
433
434
434 return err
435 return err
435
436
436 formatError = formatFailure
437 formatError = formatFailure
437
438
438 def finalize(self, result):
439 def finalize(self, result):
439 self.stream_capturer.halt()
440 self.stream_capturer.halt()
440
441
441
442
442 def run_iptest():
443 def run_iptest():
443 """Run the IPython test suite using nose.
444 """Run the IPython test suite using nose.
444
445
445 This function is called when this script is **not** called with the form
446 This function is called when this script is **not** called with the form
446 `iptest all`. It simply calls nose with appropriate command line flags
447 `iptest all`. It simply calls nose with appropriate command line flags
447 and accepts all of the standard nose arguments.
448 and accepts all of the standard nose arguments.
448 """
449 """
449 # Apply our monkeypatch to Xunit
450 # Apply our monkeypatch to Xunit
450 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
451 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
451 monkeypatch_xunit()
452 monkeypatch_xunit()
452
453
453 warnings.filterwarnings('ignore',
454 warnings.filterwarnings('ignore',
454 'This will be removed soon. Use IPython.testing.util instead')
455 'This will be removed soon. Use IPython.testing.util instead')
455
456
456 arg1 = sys.argv[1]
457 arg1 = sys.argv[1]
457 if arg1 in test_sections:
458 if arg1 in test_sections:
458 section = test_sections[arg1]
459 section = test_sections[arg1]
459 sys.argv[1:2] = section.includes
460 sys.argv[1:2] = section.includes
460 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
461 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
461 section = test_sections[arg1[8:]]
462 section = test_sections[arg1[8:]]
462 sys.argv[1:2] = section.includes
463 sys.argv[1:2] = section.includes
463 else:
464 else:
464 section = TestSection(arg1, includes=[arg1])
465 section = TestSection(arg1, includes=[arg1])
465
466
466
467
467 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
468 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
468
469
469 '--with-ipdoctest',
470 '--with-ipdoctest',
470 '--ipdoctest-tests','--ipdoctest-extension=txt',
471 '--ipdoctest-tests','--ipdoctest-extension=txt',
471
472
472 # We add --exe because of setuptools' imbecility (it
473 # We add --exe because of setuptools' imbecility (it
473 # blindly does chmod +x on ALL files). Nose does the
474 # blindly does chmod +x on ALL files). Nose does the
474 # right thing and it tries to avoid executables,
475 # right thing and it tries to avoid executables,
475 # setuptools unfortunately forces our hand here. This
476 # setuptools unfortunately forces our hand here. This
476 # has been discussed on the distutils list and the
477 # has been discussed on the distutils list and the
477 # setuptools devs refuse to fix this problem!
478 # setuptools devs refuse to fix this problem!
478 '--exe',
479 '--exe',
479 ]
480 ]
480 if '-a' not in argv and '-A' not in argv:
481 if '-a' not in argv and '-A' not in argv:
481 argv = argv + ['-a', '!crash']
482 argv = argv + ['-a', '!crash']
482
483
483 if nose.__version__ >= '0.11':
484 if nose.__version__ >= '0.11':
484 # I don't fully understand why we need this one, but depending on what
485 # I don't fully understand why we need this one, but depending on what
485 # directory the test suite is run from, if we don't give it, 0 tests
486 # directory the test suite is run from, if we don't give it, 0 tests
486 # get run. Specifically, if the test suite is run from the source dir
487 # get run. Specifically, if the test suite is run from the source dir
487 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
488 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
488 # even if the same call done in this directory works fine). It appears
489 # even if the same call done in this directory works fine). It appears
489 # that if the requested package is in the current dir, nose bails early
490 # that if the requested package is in the current dir, nose bails early
490 # by default. Since it's otherwise harmless, leave it in by default
491 # by default. Since it's otherwise harmless, leave it in by default
491 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
492 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
492 argv.append('--traverse-namespace')
493 argv.append('--traverse-namespace')
493
494
494 # use our plugin for doctesting. It will remove the standard doctest plugin
495 # use our plugin for doctesting. It will remove the standard doctest plugin
495 # if it finds it enabled
496 # if it finds it enabled
496 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
497 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
497 SubprocessStreamCapturePlugin() ]
498 SubprocessStreamCapturePlugin() ]
498
499
499 # Use working directory set by parent process (see iptestcontroller)
500 # Use working directory set by parent process (see iptestcontroller)
500 if 'IPTEST_WORKING_DIR' in os.environ:
501 if 'IPTEST_WORKING_DIR' in os.environ:
501 os.chdir(os.environ['IPTEST_WORKING_DIR'])
502 os.chdir(os.environ['IPTEST_WORKING_DIR'])
502
503
503 # We need a global ipython running in this process, but the special
504 # We need a global ipython running in this process, but the special
504 # in-process group spawns its own IPython kernels, so for *that* group we
505 # in-process group spawns its own IPython kernels, so for *that* group we
505 # must avoid also opening the global one (otherwise there's a conflict of
506 # must avoid also opening the global one (otherwise there's a conflict of
506 # singletons). Ultimately the solution to this problem is to refactor our
507 # singletons). Ultimately the solution to this problem is to refactor our
507 # assumptions about what needs to be a singleton and what doesn't (app
508 # assumptions about what needs to be a singleton and what doesn't (app
508 # objects should, individual shells shouldn't). But for now, this
509 # objects should, individual shells shouldn't). But for now, this
509 # workaround allows the test suite for the inprocess module to complete.
510 # workaround allows the test suite for the inprocess module to complete.
510 if 'kernel.inprocess' not in section.name:
511 if 'kernel.inprocess' not in section.name:
511 from IPython.testing import globalipapp
512 from IPython.testing import globalipapp
512 globalipapp.start_ipython()
513 globalipapp.start_ipython()
513
514
514 # Now nose can run
515 # Now nose can run
515 TestProgram(argv=argv, addplugins=plugins)
516 TestProgram(argv=argv, addplugins=plugins)
516
517
517 if __name__ == '__main__':
518 if __name__ == '__main__':
518 run_iptest()
519 run_iptest()
519
520
General Comments 0
You need to be logged in to leave comments. Login now