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