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