##// END OF EJS Templates
remove jupyter_console
Min RK -
Show More
@@ -1,495 +1,494 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 = ['core',
148 test_group_names = ['core',
149 'extensions', 'lib', 'terminal', 'testing', 'utils',
149 'extensions', 'lib', 'terminal', 'testing', 'utils',
150 'qt', 'html', 'nbconvert'
150 '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 'html': 'jupyter_notebook',
174 'html': 'jupyter_notebook',
175 }
175 }
176
176
177 # Name -> (include, exclude, dependencies_met)
177 # Name -> (include, exclude, dependencies_met)
178 test_sections = {n:TestSection(n, [shims.get(n, 'IPython.%s' % n)]) for n in test_group_names}
178 test_sections = {n:TestSection(n, [shims.get(n, 'IPython.%s' % n)]) for n in test_group_names}
179
179
180
180
181 # Exclusions and dependencies
181 # Exclusions and dependencies
182 # ---------------------------
182 # ---------------------------
183
183
184 # core:
184 # core:
185 sec = test_sections['core']
185 sec = test_sections['core']
186 if not have['sqlite3']:
186 if not have['sqlite3']:
187 sec.exclude('tests.test_history')
187 sec.exclude('tests.test_history')
188 sec.exclude('history')
188 sec.exclude('history')
189 if not have['matplotlib']:
189 if not have['matplotlib']:
190 sec.exclude('pylabtools'),
190 sec.exclude('pylabtools'),
191 sec.exclude('tests.test_pylabtools')
191 sec.exclude('tests.test_pylabtools')
192
192
193 # lib:
193 # lib:
194 sec = test_sections['lib']
194 sec = test_sections['lib']
195 if not have['zmq']:
195 if not have['zmq']:
196 sec.exclude('kernel')
196 sec.exclude('kernel')
197 # We do this unconditionally, so that the test suite doesn't import
197 # We do this unconditionally, so that the test suite doesn't import
198 # gtk, changing the default encoding and masking some unicode bugs.
198 # gtk, changing the default encoding and masking some unicode bugs.
199 sec.exclude('inputhookgtk')
199 sec.exclude('inputhookgtk')
200 # We also do this unconditionally, because wx can interfere with Unix signals.
200 # We also do this unconditionally, because wx can interfere with Unix signals.
201 # There are currently no tests for it anyway.
201 # There are currently no tests for it anyway.
202 sec.exclude('inputhookwx')
202 sec.exclude('inputhookwx')
203 # Testing inputhook will need a lot of thought, to figure out
203 # Testing inputhook will need a lot of thought, to figure out
204 # how to have tests that don't lock up with the gui event
204 # how to have tests that don't lock up with the gui event
205 # loops in the picture
205 # loops in the picture
206 sec.exclude('inputhook')
206 sec.exclude('inputhook')
207
207
208 # testing:
208 # testing:
209 sec = test_sections['testing']
209 sec = test_sections['testing']
210 # These have to be skipped on win32 because they use echo, rm, cd, etc.
210 # These have to be skipped on win32 because they use echo, rm, cd, etc.
211 # See ticket https://github.com/ipython/ipython/issues/87
211 # See ticket https://github.com/ipython/ipython/issues/87
212 if sys.platform == 'win32':
212 if sys.platform == 'win32':
213 sec.exclude('plugin.test_exampleip')
213 sec.exclude('plugin.test_exampleip')
214 sec.exclude('plugin.dtexample')
214 sec.exclude('plugin.dtexample')
215
215
216 # terminal:
216 # don't run jupyter_console tests found via shim
217 if (not have['pexpect']) or (not have['zmq']):
217 test_sections['terminal'].exclude('console')
218 test_sections['terminal'].exclude('console')
219
218
220 # extensions:
219 # extensions:
221 sec = test_sections['extensions']
220 sec = test_sections['extensions']
222 # This is deprecated in favour of rpy2
221 # This is deprecated in favour of rpy2
223 sec.exclude('rmagic')
222 sec.exclude('rmagic')
224 # autoreload does some strange stuff, so move it to its own test section
223 # autoreload does some strange stuff, so move it to its own test section
225 sec.exclude('autoreload')
224 sec.exclude('autoreload')
226 sec.exclude('tests.test_autoreload')
225 sec.exclude('tests.test_autoreload')
227 test_sections['autoreload'] = TestSection('autoreload',
226 test_sections['autoreload'] = TestSection('autoreload',
228 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
227 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
229 test_group_names.append('autoreload')
228 test_group_names.append('autoreload')
230
229
231 # qt:
230 # qt:
232 test_sections['qt'].requires('zmq', 'qt', 'pygments')
231 test_sections['qt'].requires('zmq', 'qt', 'pygments')
233
232
234 # html:
233 # html:
235 sec = test_sections['html']
234 sec = test_sections['html']
236 sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
235 sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
237 # The notebook 'static' directory contains JS, css and other
236 # The notebook 'static' directory contains JS, css and other
238 # files for web serving. Occasionally projects may put a .py
237 # files for web serving. Occasionally projects may put a .py
239 # file in there (MathJax ships a conf.py), so we might as
238 # file in there (MathJax ships a conf.py), so we might as
240 # well play it safe and skip the whole thing.
239 # well play it safe and skip the whole thing.
241 sec.exclude('static')
240 sec.exclude('static')
242 sec.exclude('tasks')
241 sec.exclude('tasks')
243 if not have['jinja2']:
242 if not have['jinja2']:
244 sec.exclude('notebookapp')
243 sec.exclude('notebookapp')
245 if not have['pygments'] or not have['jinja2']:
244 if not have['pygments'] or not have['jinja2']:
246 sec.exclude('nbconvert')
245 sec.exclude('nbconvert')
247 if not have['terminado']:
246 if not have['terminado']:
248 sec.exclude('terminal')
247 sec.exclude('terminal')
249
248
250 # nbconvert:
249 # nbconvert:
251 sec = test_sections['nbconvert']
250 sec = test_sections['nbconvert']
252 sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune')
251 sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune')
253 # Exclude nbconvert directories containing config files used to test.
252 # Exclude nbconvert directories containing config files used to test.
254 # Executing the config files with iptest would cause an exception.
253 # Executing the config files with iptest would cause an exception.
255 sec.exclude('tests.files')
254 sec.exclude('tests.files')
256 sec.exclude('exporters.tests.files')
255 sec.exclude('exporters.tests.files')
257 if not have['tornado']:
256 if not have['tornado']:
258 sec.exclude('nbconvert.post_processors.serve')
257 sec.exclude('nbconvert.post_processors.serve')
259 sec.exclude('nbconvert.post_processors.tests.test_serve')
258 sec.exclude('nbconvert.post_processors.tests.test_serve')
260
259
261
260
262 #-----------------------------------------------------------------------------
261 #-----------------------------------------------------------------------------
263 # Functions and classes
262 # Functions and classes
264 #-----------------------------------------------------------------------------
263 #-----------------------------------------------------------------------------
265
264
266 def check_exclusions_exist():
265 def check_exclusions_exist():
267 from IPython.utils.path import get_ipython_package_dir
266 from IPython.utils.path import get_ipython_package_dir
268 from IPython.utils.warn import warn
267 from IPython.utils.warn import warn
269 parent = os.path.dirname(get_ipython_package_dir())
268 parent = os.path.dirname(get_ipython_package_dir())
270 for sec in test_sections:
269 for sec in test_sections:
271 for pattern in sec.exclusions:
270 for pattern in sec.exclusions:
272 fullpath = pjoin(parent, pattern)
271 fullpath = pjoin(parent, pattern)
273 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
272 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
274 warn("Excluding nonexistent file: %r" % pattern)
273 warn("Excluding nonexistent file: %r" % pattern)
275
274
276
275
277 class ExclusionPlugin(Plugin):
276 class ExclusionPlugin(Plugin):
278 """A nose plugin to effect our exclusions of files and directories.
277 """A nose plugin to effect our exclusions of files and directories.
279 """
278 """
280 name = 'exclusions'
279 name = 'exclusions'
281 score = 3000 # Should come before any other plugins
280 score = 3000 # Should come before any other plugins
282
281
283 def __init__(self, exclude_patterns=None):
282 def __init__(self, exclude_patterns=None):
284 """
283 """
285 Parameters
284 Parameters
286 ----------
285 ----------
287
286
288 exclude_patterns : sequence of strings, optional
287 exclude_patterns : sequence of strings, optional
289 Filenames containing these patterns (as raw strings, not as regular
288 Filenames containing these patterns (as raw strings, not as regular
290 expressions) are excluded from the tests.
289 expressions) are excluded from the tests.
291 """
290 """
292 self.exclude_patterns = exclude_patterns or []
291 self.exclude_patterns = exclude_patterns or []
293 super(ExclusionPlugin, self).__init__()
292 super(ExclusionPlugin, self).__init__()
294
293
295 def options(self, parser, env=os.environ):
294 def options(self, parser, env=os.environ):
296 Plugin.options(self, parser, env)
295 Plugin.options(self, parser, env)
297
296
298 def configure(self, options, config):
297 def configure(self, options, config):
299 Plugin.configure(self, options, config)
298 Plugin.configure(self, options, config)
300 # Override nose trying to disable plugin.
299 # Override nose trying to disable plugin.
301 self.enabled = True
300 self.enabled = True
302
301
303 def wantFile(self, filename):
302 def wantFile(self, filename):
304 """Return whether the given filename should be scanned for tests.
303 """Return whether the given filename should be scanned for tests.
305 """
304 """
306 if any(pat in filename for pat in self.exclude_patterns):
305 if any(pat in filename for pat in self.exclude_patterns):
307 return False
306 return False
308 return None
307 return None
309
308
310 def wantDirectory(self, directory):
309 def wantDirectory(self, directory):
311 """Return whether the given directory should be scanned for tests.
310 """Return whether the given directory should be scanned for tests.
312 """
311 """
313 if any(pat in directory for pat in self.exclude_patterns):
312 if any(pat in directory for pat in self.exclude_patterns):
314 return False
313 return False
315 return None
314 return None
316
315
317
316
318 class StreamCapturer(Thread):
317 class StreamCapturer(Thread):
319 daemon = True # Don't hang if main thread crashes
318 daemon = True # Don't hang if main thread crashes
320 started = False
319 started = False
321 def __init__(self, echo=False):
320 def __init__(self, echo=False):
322 super(StreamCapturer, self).__init__()
321 super(StreamCapturer, self).__init__()
323 self.echo = echo
322 self.echo = echo
324 self.streams = []
323 self.streams = []
325 self.buffer = BytesIO()
324 self.buffer = BytesIO()
326 self.readfd, self.writefd = os.pipe()
325 self.readfd, self.writefd = os.pipe()
327 self.buffer_lock = Lock()
326 self.buffer_lock = Lock()
328 self.stop = Event()
327 self.stop = Event()
329
328
330 def run(self):
329 def run(self):
331 self.started = True
330 self.started = True
332
331
333 while not self.stop.is_set():
332 while not self.stop.is_set():
334 chunk = os.read(self.readfd, 1024)
333 chunk = os.read(self.readfd, 1024)
335
334
336 with self.buffer_lock:
335 with self.buffer_lock:
337 self.buffer.write(chunk)
336 self.buffer.write(chunk)
338 if self.echo:
337 if self.echo:
339 sys.stdout.write(bytes_to_str(chunk))
338 sys.stdout.write(bytes_to_str(chunk))
340
339
341 os.close(self.readfd)
340 os.close(self.readfd)
342 os.close(self.writefd)
341 os.close(self.writefd)
343
342
344 def reset_buffer(self):
343 def reset_buffer(self):
345 with self.buffer_lock:
344 with self.buffer_lock:
346 self.buffer.truncate(0)
345 self.buffer.truncate(0)
347 self.buffer.seek(0)
346 self.buffer.seek(0)
348
347
349 def get_buffer(self):
348 def get_buffer(self):
350 with self.buffer_lock:
349 with self.buffer_lock:
351 return self.buffer.getvalue()
350 return self.buffer.getvalue()
352
351
353 def ensure_started(self):
352 def ensure_started(self):
354 if not self.started:
353 if not self.started:
355 self.start()
354 self.start()
356
355
357 def halt(self):
356 def halt(self):
358 """Safely stop the thread."""
357 """Safely stop the thread."""
359 if not self.started:
358 if not self.started:
360 return
359 return
361
360
362 self.stop.set()
361 self.stop.set()
363 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
362 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
364 self.join()
363 self.join()
365
364
366 class SubprocessStreamCapturePlugin(Plugin):
365 class SubprocessStreamCapturePlugin(Plugin):
367 name='subprocstreams'
366 name='subprocstreams'
368 def __init__(self):
367 def __init__(self):
369 Plugin.__init__(self)
368 Plugin.__init__(self)
370 self.stream_capturer = StreamCapturer()
369 self.stream_capturer = StreamCapturer()
371 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
370 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
372 # This is ugly, but distant parts of the test machinery need to be able
371 # This is ugly, but distant parts of the test machinery need to be able
373 # to redirect streams, so we make the object globally accessible.
372 # to redirect streams, so we make the object globally accessible.
374 nose.iptest_stdstreams_fileno = self.get_write_fileno
373 nose.iptest_stdstreams_fileno = self.get_write_fileno
375
374
376 def get_write_fileno(self):
375 def get_write_fileno(self):
377 if self.destination == 'capture':
376 if self.destination == 'capture':
378 self.stream_capturer.ensure_started()
377 self.stream_capturer.ensure_started()
379 return self.stream_capturer.writefd
378 return self.stream_capturer.writefd
380 elif self.destination == 'discard':
379 elif self.destination == 'discard':
381 return os.open(os.devnull, os.O_WRONLY)
380 return os.open(os.devnull, os.O_WRONLY)
382 else:
381 else:
383 return sys.__stdout__.fileno()
382 return sys.__stdout__.fileno()
384
383
385 def configure(self, options, config):
384 def configure(self, options, config):
386 Plugin.configure(self, options, config)
385 Plugin.configure(self, options, config)
387 # Override nose trying to disable plugin.
386 # Override nose trying to disable plugin.
388 if self.destination == 'capture':
387 if self.destination == 'capture':
389 self.enabled = True
388 self.enabled = True
390
389
391 def startTest(self, test):
390 def startTest(self, test):
392 # Reset log capture
391 # Reset log capture
393 self.stream_capturer.reset_buffer()
392 self.stream_capturer.reset_buffer()
394
393
395 def formatFailure(self, test, err):
394 def formatFailure(self, test, err):
396 # Show output
395 # Show output
397 ec, ev, tb = err
396 ec, ev, tb = err
398 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
397 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
399 if captured.strip():
398 if captured.strip():
400 ev = safe_str(ev)
399 ev = safe_str(ev)
401 out = [ev, '>> begin captured subprocess output <<',
400 out = [ev, '>> begin captured subprocess output <<',
402 captured,
401 captured,
403 '>> end captured subprocess output <<']
402 '>> end captured subprocess output <<']
404 return ec, '\n'.join(out), tb
403 return ec, '\n'.join(out), tb
405
404
406 return err
405 return err
407
406
408 formatError = formatFailure
407 formatError = formatFailure
409
408
410 def finalize(self, result):
409 def finalize(self, result):
411 self.stream_capturer.halt()
410 self.stream_capturer.halt()
412
411
413
412
414 def run_iptest():
413 def run_iptest():
415 """Run the IPython test suite using nose.
414 """Run the IPython test suite using nose.
416
415
417 This function is called when this script is **not** called with the form
416 This function is called when this script is **not** called with the form
418 `iptest all`. It simply calls nose with appropriate command line flags
417 `iptest all`. It simply calls nose with appropriate command line flags
419 and accepts all of the standard nose arguments.
418 and accepts all of the standard nose arguments.
420 """
419 """
421 # Apply our monkeypatch to Xunit
420 # Apply our monkeypatch to Xunit
422 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
421 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
423 monkeypatch_xunit()
422 monkeypatch_xunit()
424
423
425 warnings.filterwarnings('ignore',
424 warnings.filterwarnings('ignore',
426 'This will be removed soon. Use IPython.testing.util instead')
425 'This will be removed soon. Use IPython.testing.util instead')
427
426
428 arg1 = sys.argv[1]
427 arg1 = sys.argv[1]
429 if arg1 in test_sections:
428 if arg1 in test_sections:
430 section = test_sections[arg1]
429 section = test_sections[arg1]
431 sys.argv[1:2] = section.includes
430 sys.argv[1:2] = section.includes
432 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
431 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
433 section = test_sections[arg1[8:]]
432 section = test_sections[arg1[8:]]
434 sys.argv[1:2] = section.includes
433 sys.argv[1:2] = section.includes
435 else:
434 else:
436 section = TestSection(arg1, includes=[arg1])
435 section = TestSection(arg1, includes=[arg1])
437
436
438
437
439 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
438 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
440 # We add --exe because of setuptools' imbecility (it
439 # We add --exe because of setuptools' imbecility (it
441 # blindly does chmod +x on ALL files). Nose does the
440 # blindly does chmod +x on ALL files). Nose does the
442 # right thing and it tries to avoid executables,
441 # right thing and it tries to avoid executables,
443 # setuptools unfortunately forces our hand here. This
442 # setuptools unfortunately forces our hand here. This
444 # has been discussed on the distutils list and the
443 # has been discussed on the distutils list and the
445 # setuptools devs refuse to fix this problem!
444 # setuptools devs refuse to fix this problem!
446 '--exe',
445 '--exe',
447 ]
446 ]
448 if '-a' not in argv and '-A' not in argv:
447 if '-a' not in argv and '-A' not in argv:
449 argv = argv + ['-a', '!crash']
448 argv = argv + ['-a', '!crash']
450
449
451 if nose.__version__ >= '0.11':
450 if nose.__version__ >= '0.11':
452 # I don't fully understand why we need this one, but depending on what
451 # I don't fully understand why we need this one, but depending on what
453 # directory the test suite is run from, if we don't give it, 0 tests
452 # directory the test suite is run from, if we don't give it, 0 tests
454 # get run. Specifically, if the test suite is run from the source dir
453 # get run. Specifically, if the test suite is run from the source dir
455 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
454 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
456 # even if the same call done in this directory works fine). It appears
455 # even if the same call done in this directory works fine). It appears
457 # that if the requested package is in the current dir, nose bails early
456 # that if the requested package is in the current dir, nose bails early
458 # by default. Since it's otherwise harmless, leave it in by default
457 # by default. Since it's otherwise harmless, leave it in by default
459 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
458 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
460 argv.append('--traverse-namespace')
459 argv.append('--traverse-namespace')
461
460
462 plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
461 plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
463 SubprocessStreamCapturePlugin() ]
462 SubprocessStreamCapturePlugin() ]
464
463
465 # we still have some vestigial doctests in core
464 # we still have some vestigial doctests in core
466 if (section.name.startswith(('core', 'IPython.core'))):
465 if (section.name.startswith(('core', 'IPython.core'))):
467 plugins.append(IPythonDoctest())
466 plugins.append(IPythonDoctest())
468 argv.extend([
467 argv.extend([
469 '--with-ipdoctest',
468 '--with-ipdoctest',
470 '--ipdoctest-tests',
469 '--ipdoctest-tests',
471 '--ipdoctest-extension=txt',
470 '--ipdoctest-extension=txt',
472 ])
471 ])
473
472
474
473
475 # Use working directory set by parent process (see iptestcontroller)
474 # Use working directory set by parent process (see iptestcontroller)
476 if 'IPTEST_WORKING_DIR' in os.environ:
475 if 'IPTEST_WORKING_DIR' in os.environ:
477 os.chdir(os.environ['IPTEST_WORKING_DIR'])
476 os.chdir(os.environ['IPTEST_WORKING_DIR'])
478
477
479 # We need a global ipython running in this process, but the special
478 # We need a global ipython running in this process, but the special
480 # in-process group spawns its own IPython kernels, so for *that* group we
479 # in-process group spawns its own IPython kernels, so for *that* group we
481 # must avoid also opening the global one (otherwise there's a conflict of
480 # must avoid also opening the global one (otherwise there's a conflict of
482 # singletons). Ultimately the solution to this problem is to refactor our
481 # singletons). Ultimately the solution to this problem is to refactor our
483 # assumptions about what needs to be a singleton and what doesn't (app
482 # assumptions about what needs to be a singleton and what doesn't (app
484 # objects should, individual shells shouldn't). But for now, this
483 # objects should, individual shells shouldn't). But for now, this
485 # workaround allows the test suite for the inprocess module to complete.
484 # workaround allows the test suite for the inprocess module to complete.
486 if 'kernel.inprocess' not in section.name:
485 if 'kernel.inprocess' not in section.name:
487 from IPython.testing import globalipapp
486 from IPython.testing import globalipapp
488 globalipapp.start_ipython()
487 globalipapp.start_ipython()
489
488
490 # Now nose can run
489 # Now nose can run
491 TestProgram(argv=argv, addplugins=plugins)
490 TestProgram(argv=argv, addplugins=plugins)
492
491
493 if __name__ == '__main__':
492 if __name__ == '__main__':
494 run_iptest()
493 run_iptest()
495
494
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (585 lines changed) Show them Hide them
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now