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