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