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