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