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