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