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