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