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