##// END OF EJS Templates
distinguish capture_output from buffer_output...
Min RK -
Show More
@@ -1,515 +1,519 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.importstring import import_item
39 from IPython.utils.importstring import import_item
39 from IPython.testing.plugin.ipdoctest import IPythonDoctest
40 from IPython.testing.plugin.ipdoctest import IPythonDoctest
40 from IPython.external.decorators import KnownFailure, knownfailureif
41 from IPython.external.decorators import KnownFailure, knownfailureif
41
42
42 pjoin = path.join
43 pjoin = path.join
43
44
44
45
45 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
46 # Globals
47 # Globals
47 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
48
49
49
50
50 #-----------------------------------------------------------------------------
51 #-----------------------------------------------------------------------------
51 # Warnings control
52 # Warnings control
52 #-----------------------------------------------------------------------------
53 #-----------------------------------------------------------------------------
53
54
54 # Twisted generates annoying warnings with Python 2.6, as will do other code
55 # Twisted generates annoying warnings with Python 2.6, as will do other code
55 # that imports 'sets' as of today
56 # that imports 'sets' as of today
56 warnings.filterwarnings('ignore', 'the sets module is deprecated',
57 warnings.filterwarnings('ignore', 'the sets module is deprecated',
57 DeprecationWarning )
58 DeprecationWarning )
58
59
59 # This one also comes from Twisted
60 # This one also comes from Twisted
60 warnings.filterwarnings('ignore', 'the sha module is deprecated',
61 warnings.filterwarnings('ignore', 'the sha module is deprecated',
61 DeprecationWarning)
62 DeprecationWarning)
62
63
63 # Wx on Fedora11 spits these out
64 # Wx on Fedora11 spits these out
64 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
65 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
65 UserWarning)
66 UserWarning)
66
67
67 # ------------------------------------------------------------------------------
68 # ------------------------------------------------------------------------------
68 # Monkeypatch Xunit to count known failures as skipped.
69 # Monkeypatch Xunit to count known failures as skipped.
69 # ------------------------------------------------------------------------------
70 # ------------------------------------------------------------------------------
70 def monkeypatch_xunit():
71 def monkeypatch_xunit():
71 try:
72 try:
72 knownfailureif(True)(lambda: None)()
73 knownfailureif(True)(lambda: None)()
73 except Exception as e:
74 except Exception as e:
74 KnownFailureTest = type(e)
75 KnownFailureTest = type(e)
75
76
76 def addError(self, test, err, capt=None):
77 def addError(self, test, err, capt=None):
77 if issubclass(err[0], KnownFailureTest):
78 if issubclass(err[0], KnownFailureTest):
78 err = (SkipTest,) + err[1:]
79 err = (SkipTest,) + err[1:]
79 return self.orig_addError(test, err, capt)
80 return self.orig_addError(test, err, capt)
80
81
81 Xunit.orig_addError = Xunit.addError
82 Xunit.orig_addError = Xunit.addError
82 Xunit.addError = addError
83 Xunit.addError = addError
83
84
84 #-----------------------------------------------------------------------------
85 #-----------------------------------------------------------------------------
85 # Check which dependencies are installed and greater than minimum version.
86 # Check which dependencies are installed and greater than minimum version.
86 #-----------------------------------------------------------------------------
87 #-----------------------------------------------------------------------------
87 def extract_version(mod):
88 def extract_version(mod):
88 return mod.__version__
89 return mod.__version__
89
90
90 def test_for(item, min_version=None, callback=extract_version):
91 def test_for(item, min_version=None, callback=extract_version):
91 """Test to see if item is importable, and optionally check against a minimum
92 """Test to see if item is importable, and optionally check against a minimum
92 version.
93 version.
93
94
94 If min_version is given, the default behavior is to check against the
95 If min_version is given, the default behavior is to check against the
95 `__version__` attribute of the item, but specifying `callback` allows you to
96 `__version__` attribute of the item, but specifying `callback` allows you to
96 extract the value you are interested in. e.g::
97 extract the value you are interested in. e.g::
97
98
98 In [1]: import sys
99 In [1]: import sys
99
100
100 In [2]: from IPython.testing.iptest import test_for
101 In [2]: from IPython.testing.iptest import test_for
101
102
102 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
103 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
103 Out[3]: True
104 Out[3]: True
104
105
105 """
106 """
106 try:
107 try:
107 check = import_item(item)
108 check = import_item(item)
108 except (ImportError, RuntimeError):
109 except (ImportError, RuntimeError):
109 # GTK reports Runtime error if it can't be initialized even if it's
110 # GTK reports Runtime error if it can't be initialized even if it's
110 # importable.
111 # importable.
111 return False
112 return False
112 else:
113 else:
113 if min_version:
114 if min_version:
114 if callback:
115 if callback:
115 # extra processing step to get version to compare
116 # extra processing step to get version to compare
116 check = callback(check)
117 check = callback(check)
117
118
118 return check >= min_version
119 return check >= min_version
119 else:
120 else:
120 return True
121 return True
121
122
122 # Global dict where we can store information on what we have and what we don't
123 # Global dict where we can store information on what we have and what we don't
123 # have available at test run time
124 # have available at test run time
124 have = {}
125 have = {}
125
126
126 have['curses'] = test_for('_curses')
127 have['curses'] = test_for('_curses')
127 have['matplotlib'] = test_for('matplotlib')
128 have['matplotlib'] = test_for('matplotlib')
128 have['numpy'] = test_for('numpy')
129 have['numpy'] = test_for('numpy')
129 have['pexpect'] = test_for('IPython.external.pexpect')
130 have['pexpect'] = test_for('IPython.external.pexpect')
130 have['pymongo'] = test_for('pymongo')
131 have['pymongo'] = test_for('pymongo')
131 have['pygments'] = test_for('pygments')
132 have['pygments'] = test_for('pygments')
132 have['qt'] = test_for('IPython.external.qt')
133 have['qt'] = test_for('IPython.external.qt')
133 have['sqlite3'] = test_for('sqlite3')
134 have['sqlite3'] = test_for('sqlite3')
134 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
135 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
135 have['jinja2'] = test_for('jinja2')
136 have['jinja2'] = test_for('jinja2')
136 have['mistune'] = test_for('mistune')
137 have['mistune'] = test_for('mistune')
137 have['requests'] = test_for('requests')
138 have['requests'] = test_for('requests')
138 have['sphinx'] = test_for('sphinx')
139 have['sphinx'] = test_for('sphinx')
139 have['jsonschema'] = test_for('jsonschema')
140 have['jsonschema'] = test_for('jsonschema')
140 have['terminado'] = test_for('terminado')
141 have['terminado'] = test_for('terminado')
141 have['casperjs'] = is_cmd_found('casperjs')
142 have['casperjs'] = is_cmd_found('casperjs')
142 have['phantomjs'] = is_cmd_found('phantomjs')
143 have['phantomjs'] = is_cmd_found('phantomjs')
143 have['slimerjs'] = is_cmd_found('slimerjs')
144 have['slimerjs'] = is_cmd_found('slimerjs')
144
145
145 min_zmq = (2,1,11)
146 min_zmq = (2,1,11)
146
147
147 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
148 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
148
149
149 #-----------------------------------------------------------------------------
150 #-----------------------------------------------------------------------------
150 # Test suite definitions
151 # Test suite definitions
151 #-----------------------------------------------------------------------------
152 #-----------------------------------------------------------------------------
152
153
153 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
154 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
154 'extensions', 'lib', 'terminal', 'testing', 'utils',
155 'extensions', 'lib', 'terminal', 'testing', 'utils',
155 'nbformat', 'qt', 'html', 'nbconvert'
156 'nbformat', 'qt', 'html', 'nbconvert'
156 ]
157 ]
157
158
158 class TestSection(object):
159 class TestSection(object):
159 def __init__(self, name, includes):
160 def __init__(self, name, includes):
160 self.name = name
161 self.name = name
161 self.includes = includes
162 self.includes = includes
162 self.excludes = []
163 self.excludes = []
163 self.dependencies = []
164 self.dependencies = []
164 self.enabled = True
165 self.enabled = True
165
166
166 def exclude(self, module):
167 def exclude(self, module):
167 if not module.startswith('IPython'):
168 if not module.startswith('IPython'):
168 module = self.includes[0] + "." + module
169 module = self.includes[0] + "." + module
169 self.excludes.append(module.replace('.', os.sep))
170 self.excludes.append(module.replace('.', os.sep))
170
171
171 def requires(self, *packages):
172 def requires(self, *packages):
172 self.dependencies.extend(packages)
173 self.dependencies.extend(packages)
173
174
174 @property
175 @property
175 def will_run(self):
176 def will_run(self):
176 return self.enabled and all(have[p] for p in self.dependencies)
177 return self.enabled and all(have[p] for p in self.dependencies)
177
178
178 # Name -> (include, exclude, dependencies_met)
179 # Name -> (include, exclude, dependencies_met)
179 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
180 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
180
181
181 # Exclusions and dependencies
182 # Exclusions and dependencies
182 # ---------------------------
183 # ---------------------------
183
184
184 # core:
185 # core:
185 sec = test_sections['core']
186 sec = test_sections['core']
186 if not have['sqlite3']:
187 if not have['sqlite3']:
187 sec.exclude('tests.test_history')
188 sec.exclude('tests.test_history')
188 sec.exclude('history')
189 sec.exclude('history')
189 if not have['matplotlib']:
190 if not have['matplotlib']:
190 sec.exclude('pylabtools'),
191 sec.exclude('pylabtools'),
191 sec.exclude('tests.test_pylabtools')
192 sec.exclude('tests.test_pylabtools')
192
193
193 # lib:
194 # lib:
194 sec = test_sections['lib']
195 sec = test_sections['lib']
195 if not have['zmq']:
196 if not have['zmq']:
196 sec.exclude('kernel')
197 sec.exclude('kernel')
197 # We do this unconditionally, so that the test suite doesn't import
198 # We do this unconditionally, so that the test suite doesn't import
198 # gtk, changing the default encoding and masking some unicode bugs.
199 # gtk, changing the default encoding and masking some unicode bugs.
199 sec.exclude('inputhookgtk')
200 sec.exclude('inputhookgtk')
200 # We also do this unconditionally, because wx can interfere with Unix signals.
201 # We also do this unconditionally, because wx can interfere with Unix signals.
201 # There are currently no tests for it anyway.
202 # There are currently no tests for it anyway.
202 sec.exclude('inputhookwx')
203 sec.exclude('inputhookwx')
203 # Testing inputhook will need a lot of thought, to figure out
204 # 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
205 # how to have tests that don't lock up with the gui event
205 # loops in the picture
206 # loops in the picture
206 sec.exclude('inputhook')
207 sec.exclude('inputhook')
207
208
208 # testing:
209 # testing:
209 sec = test_sections['testing']
210 sec = test_sections['testing']
210 # These have to be skipped on win32 because they use echo, rm, cd, etc.
211 # These have to be skipped on win32 because they use echo, rm, cd, etc.
211 # See ticket https://github.com/ipython/ipython/issues/87
212 # See ticket https://github.com/ipython/ipython/issues/87
212 if sys.platform == 'win32':
213 if sys.platform == 'win32':
213 sec.exclude('plugin.test_exampleip')
214 sec.exclude('plugin.test_exampleip')
214 sec.exclude('plugin.dtexample')
215 sec.exclude('plugin.dtexample')
215
216
216 # terminal:
217 # terminal:
217 if (not have['pexpect']) or (not have['zmq']):
218 if (not have['pexpect']) or (not have['zmq']):
218 test_sections['terminal'].exclude('console')
219 test_sections['terminal'].exclude('console')
219
220
220 # parallel
221 # parallel
221 sec = test_sections['parallel']
222 sec = test_sections['parallel']
222 sec.requires('zmq')
223 sec.requires('zmq')
223 if not have['pymongo']:
224 if not have['pymongo']:
224 sec.exclude('controller.mongodb')
225 sec.exclude('controller.mongodb')
225 sec.exclude('tests.test_mongodb')
226 sec.exclude('tests.test_mongodb')
226
227
227 # kernel:
228 # kernel:
228 sec = test_sections['kernel']
229 sec = test_sections['kernel']
229 sec.requires('zmq')
230 sec.requires('zmq')
230 # The in-process kernel tests are done in a separate section
231 # The in-process kernel tests are done in a separate section
231 sec.exclude('inprocess')
232 sec.exclude('inprocess')
232 # importing gtk sets the default encoding, which we want to avoid
233 # importing gtk sets the default encoding, which we want to avoid
233 sec.exclude('zmq.gui.gtkembed')
234 sec.exclude('zmq.gui.gtkembed')
234 sec.exclude('zmq.gui.gtk3embed')
235 sec.exclude('zmq.gui.gtk3embed')
235 if not have['matplotlib']:
236 if not have['matplotlib']:
236 sec.exclude('zmq.pylab')
237 sec.exclude('zmq.pylab')
237
238
238 # kernel.inprocess:
239 # kernel.inprocess:
239 test_sections['kernel.inprocess'].requires('zmq')
240 test_sections['kernel.inprocess'].requires('zmq')
240
241
241 # extensions:
242 # extensions:
242 sec = test_sections['extensions']
243 sec = test_sections['extensions']
243 # This is deprecated in favour of rpy2
244 # This is deprecated in favour of rpy2
244 sec.exclude('rmagic')
245 sec.exclude('rmagic')
245 # autoreload does some strange stuff, so move it to its own test section
246 # autoreload does some strange stuff, so move it to its own test section
246 sec.exclude('autoreload')
247 sec.exclude('autoreload')
247 sec.exclude('tests.test_autoreload')
248 sec.exclude('tests.test_autoreload')
248 test_sections['autoreload'] = TestSection('autoreload',
249 test_sections['autoreload'] = TestSection('autoreload',
249 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
250 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
250 test_group_names.append('autoreload')
251 test_group_names.append('autoreload')
251
252
252 # qt:
253 # qt:
253 test_sections['qt'].requires('zmq', 'qt', 'pygments')
254 test_sections['qt'].requires('zmq', 'qt', 'pygments')
254
255
255 # html:
256 # html:
256 sec = test_sections['html']
257 sec = test_sections['html']
257 sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
258 sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
258 # The notebook 'static' directory contains JS, css and other
259 # The notebook 'static' directory contains JS, css and other
259 # files for web serving. Occasionally projects may put a .py
260 # files for web serving. Occasionally projects may put a .py
260 # file in there (MathJax ships a conf.py), so we might as
261 # file in there (MathJax ships a conf.py), so we might as
261 # well play it safe and skip the whole thing.
262 # well play it safe and skip the whole thing.
262 sec.exclude('static')
263 sec.exclude('static')
263 sec.exclude('tasks')
264 sec.exclude('tasks')
264 if not have['jinja2']:
265 if not have['jinja2']:
265 sec.exclude('notebookapp')
266 sec.exclude('notebookapp')
266 if not have['pygments'] or not have['jinja2']:
267 if not have['pygments'] or not have['jinja2']:
267 sec.exclude('nbconvert')
268 sec.exclude('nbconvert')
268 if not have['terminado']:
269 if not have['terminado']:
269 sec.exclude('terminal')
270 sec.exclude('terminal')
270
271
271 # config:
272 # config:
272 # Config files aren't really importable stand-alone
273 # Config files aren't really importable stand-alone
273 test_sections['config'].exclude('profile')
274 test_sections['config'].exclude('profile')
274
275
275 # nbconvert:
276 # nbconvert:
276 sec = test_sections['nbconvert']
277 sec = test_sections['nbconvert']
277 sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune')
278 sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune')
278 # Exclude nbconvert directories containing config files used to test.
279 # Exclude nbconvert directories containing config files used to test.
279 # Executing the config files with iptest would cause an exception.
280 # Executing the config files with iptest would cause an exception.
280 sec.exclude('tests.files')
281 sec.exclude('tests.files')
281 sec.exclude('exporters.tests.files')
282 sec.exclude('exporters.tests.files')
282 if not have['tornado']:
283 if not have['tornado']:
283 sec.exclude('nbconvert.post_processors.serve')
284 sec.exclude('nbconvert.post_processors.serve')
284 sec.exclude('nbconvert.post_processors.tests.test_serve')
285 sec.exclude('nbconvert.post_processors.tests.test_serve')
285
286
286 # nbformat:
287 # nbformat:
287 test_sections['nbformat'].requires('jsonschema')
288 test_sections['nbformat'].requires('jsonschema')
288
289
289 #-----------------------------------------------------------------------------
290 #-----------------------------------------------------------------------------
290 # Functions and classes
291 # Functions and classes
291 #-----------------------------------------------------------------------------
292 #-----------------------------------------------------------------------------
292
293
293 def check_exclusions_exist():
294 def check_exclusions_exist():
294 from IPython.utils.path import get_ipython_package_dir
295 from IPython.utils.path import get_ipython_package_dir
295 from IPython.utils.warn import warn
296 from IPython.utils.warn import warn
296 parent = os.path.dirname(get_ipython_package_dir())
297 parent = os.path.dirname(get_ipython_package_dir())
297 for sec in test_sections:
298 for sec in test_sections:
298 for pattern in sec.exclusions:
299 for pattern in sec.exclusions:
299 fullpath = pjoin(parent, pattern)
300 fullpath = pjoin(parent, pattern)
300 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
301 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
301 warn("Excluding nonexistent file: %r" % pattern)
302 warn("Excluding nonexistent file: %r" % pattern)
302
303
303
304
304 class ExclusionPlugin(Plugin):
305 class ExclusionPlugin(Plugin):
305 """A nose plugin to effect our exclusions of files and directories.
306 """A nose plugin to effect our exclusions of files and directories.
306 """
307 """
307 name = 'exclusions'
308 name = 'exclusions'
308 score = 3000 # Should come before any other plugins
309 score = 3000 # Should come before any other plugins
309
310
310 def __init__(self, exclude_patterns=None):
311 def __init__(self, exclude_patterns=None):
311 """
312 """
312 Parameters
313 Parameters
313 ----------
314 ----------
314
315
315 exclude_patterns : sequence of strings, optional
316 exclude_patterns : sequence of strings, optional
316 Filenames containing these patterns (as raw strings, not as regular
317 Filenames containing these patterns (as raw strings, not as regular
317 expressions) are excluded from the tests.
318 expressions) are excluded from the tests.
318 """
319 """
319 self.exclude_patterns = exclude_patterns or []
320 self.exclude_patterns = exclude_patterns or []
320 super(ExclusionPlugin, self).__init__()
321 super(ExclusionPlugin, self).__init__()
321
322
322 def options(self, parser, env=os.environ):
323 def options(self, parser, env=os.environ):
323 Plugin.options(self, parser, env)
324 Plugin.options(self, parser, env)
324
325
325 def configure(self, options, config):
326 def configure(self, options, config):
326 Plugin.configure(self, options, config)
327 Plugin.configure(self, options, config)
327 # Override nose trying to disable plugin.
328 # Override nose trying to disable plugin.
328 self.enabled = True
329 self.enabled = True
329
330
330 def wantFile(self, filename):
331 def wantFile(self, filename):
331 """Return whether the given filename should be scanned for tests.
332 """Return whether the given filename should be scanned for tests.
332 """
333 """
333 if any(pat in filename for pat in self.exclude_patterns):
334 if any(pat in filename for pat in self.exclude_patterns):
334 return False
335 return False
335 return None
336 return None
336
337
337 def wantDirectory(self, directory):
338 def wantDirectory(self, directory):
338 """Return whether the given directory should be scanned for tests.
339 """Return whether the given directory should be scanned for tests.
339 """
340 """
340 if any(pat in directory for pat in self.exclude_patterns):
341 if any(pat in directory for pat in self.exclude_patterns):
341 return False
342 return False
342 return None
343 return None
343
344
344
345
345 class StreamCapturer(Thread):
346 class StreamCapturer(Thread):
346 daemon = True # Don't hang if main thread crashes
347 daemon = True # Don't hang if main thread crashes
347 started = False
348 started = False
348 def __init__(self):
349 def __init__(self, echo=False):
349 super(StreamCapturer, self).__init__()
350 super(StreamCapturer, self).__init__()
351 self.echo = echo
350 self.streams = []
352 self.streams = []
351 self.buffer = BytesIO()
353 self.buffer = BytesIO()
352 self.readfd, self.writefd = os.pipe()
354 self.readfd, self.writefd = os.pipe()
353 self.buffer_lock = Lock()
355 self.buffer_lock = Lock()
354 self.stop = Event()
356 self.stop = Event()
355
357
356 def run(self):
358 def run(self):
357 self.started = True
359 self.started = True
358
360
359 while not self.stop.is_set():
361 while not self.stop.is_set():
360 chunk = os.read(self.readfd, 1024)
362 chunk = os.read(self.readfd, 1024)
361
363
362 with self.buffer_lock:
364 with self.buffer_lock:
363 self.buffer.write(chunk)
365 self.buffer.write(chunk)
366 if self.echo:
367 sys.stdout.write(bytes_to_str(chunk))
364
368
365 os.close(self.readfd)
369 os.close(self.readfd)
366 os.close(self.writefd)
370 os.close(self.writefd)
367
371
368 def reset_buffer(self):
372 def reset_buffer(self):
369 with self.buffer_lock:
373 with self.buffer_lock:
370 self.buffer.truncate(0)
374 self.buffer.truncate(0)
371 self.buffer.seek(0)
375 self.buffer.seek(0)
372
376
373 def get_buffer(self):
377 def get_buffer(self):
374 with self.buffer_lock:
378 with self.buffer_lock:
375 return self.buffer.getvalue()
379 return self.buffer.getvalue()
376
380
377 def ensure_started(self):
381 def ensure_started(self):
378 if not self.started:
382 if not self.started:
379 self.start()
383 self.start()
380
384
381 def halt(self):
385 def halt(self):
382 """Safely stop the thread."""
386 """Safely stop the thread."""
383 if not self.started:
387 if not self.started:
384 return
388 return
385
389
386 self.stop.set()
390 self.stop.set()
387 os.write(self.writefd, b'wake up') # Ensure we're not locked in a read()
391 os.write(self.writefd, b'wake up') # Ensure we're not locked in a read()
388 self.join()
392 self.join()
389
393
390 class SubprocessStreamCapturePlugin(Plugin):
394 class SubprocessStreamCapturePlugin(Plugin):
391 name='subprocstreams'
395 name='subprocstreams'
392 def __init__(self):
396 def __init__(self):
393 Plugin.__init__(self)
397 Plugin.__init__(self)
394 self.stream_capturer = StreamCapturer()
398 self.stream_capturer = StreamCapturer()
395 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
399 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
396 # This is ugly, but distant parts of the test machinery need to be able
400 # This is ugly, but distant parts of the test machinery need to be able
397 # to redirect streams, so we make the object globally accessible.
401 # to redirect streams, so we make the object globally accessible.
398 nose.iptest_stdstreams_fileno = self.get_write_fileno
402 nose.iptest_stdstreams_fileno = self.get_write_fileno
399
403
400 def get_write_fileno(self):
404 def get_write_fileno(self):
401 if self.destination == 'capture':
405 if self.destination == 'capture':
402 self.stream_capturer.ensure_started()
406 self.stream_capturer.ensure_started()
403 return self.stream_capturer.writefd
407 return self.stream_capturer.writefd
404 elif self.destination == 'discard':
408 elif self.destination == 'discard':
405 return os.open(os.devnull, os.O_WRONLY)
409 return os.open(os.devnull, os.O_WRONLY)
406 else:
410 else:
407 return sys.__stdout__.fileno()
411 return sys.__stdout__.fileno()
408
412
409 def configure(self, options, config):
413 def configure(self, options, config):
410 Plugin.configure(self, options, config)
414 Plugin.configure(self, options, config)
411 # Override nose trying to disable plugin.
415 # Override nose trying to disable plugin.
412 if self.destination == 'capture':
416 if self.destination == 'capture':
413 self.enabled = True
417 self.enabled = True
414
418
415 def startTest(self, test):
419 def startTest(self, test):
416 # Reset log capture
420 # Reset log capture
417 self.stream_capturer.reset_buffer()
421 self.stream_capturer.reset_buffer()
418
422
419 def formatFailure(self, test, err):
423 def formatFailure(self, test, err):
420 # Show output
424 # Show output
421 ec, ev, tb = err
425 ec, ev, tb = err
422 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
426 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
423 if captured.strip():
427 if captured.strip():
424 ev = safe_str(ev)
428 ev = safe_str(ev)
425 out = [ev, '>> begin captured subprocess output <<',
429 out = [ev, '>> begin captured subprocess output <<',
426 captured,
430 captured,
427 '>> end captured subprocess output <<']
431 '>> end captured subprocess output <<']
428 return ec, '\n'.join(out), tb
432 return ec, '\n'.join(out), tb
429
433
430 return err
434 return err
431
435
432 formatError = formatFailure
436 formatError = formatFailure
433
437
434 def finalize(self, result):
438 def finalize(self, result):
435 self.stream_capturer.halt()
439 self.stream_capturer.halt()
436
440
437
441
438 def run_iptest():
442 def run_iptest():
439 """Run the IPython test suite using nose.
443 """Run the IPython test suite using nose.
440
444
441 This function is called when this script is **not** called with the form
445 This function is called when this script is **not** called with the form
442 `iptest all`. It simply calls nose with appropriate command line flags
446 `iptest all`. It simply calls nose with appropriate command line flags
443 and accepts all of the standard nose arguments.
447 and accepts all of the standard nose arguments.
444 """
448 """
445 # Apply our monkeypatch to Xunit
449 # Apply our monkeypatch to Xunit
446 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
450 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
447 monkeypatch_xunit()
451 monkeypatch_xunit()
448
452
449 warnings.filterwarnings('ignore',
453 warnings.filterwarnings('ignore',
450 'This will be removed soon. Use IPython.testing.util instead')
454 'This will be removed soon. Use IPython.testing.util instead')
451
455
452 arg1 = sys.argv[1]
456 arg1 = sys.argv[1]
453 if arg1 in test_sections:
457 if arg1 in test_sections:
454 section = test_sections[arg1]
458 section = test_sections[arg1]
455 sys.argv[1:2] = section.includes
459 sys.argv[1:2] = section.includes
456 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
460 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
457 section = test_sections[arg1[8:]]
461 section = test_sections[arg1[8:]]
458 sys.argv[1:2] = section.includes
462 sys.argv[1:2] = section.includes
459 else:
463 else:
460 section = TestSection(arg1, includes=[arg1])
464 section = TestSection(arg1, includes=[arg1])
461
465
462
466
463 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
467 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
464
468
465 '--with-ipdoctest',
469 '--with-ipdoctest',
466 '--ipdoctest-tests','--ipdoctest-extension=txt',
470 '--ipdoctest-tests','--ipdoctest-extension=txt',
467
471
468 # We add --exe because of setuptools' imbecility (it
472 # We add --exe because of setuptools' imbecility (it
469 # blindly does chmod +x on ALL files). Nose does the
473 # blindly does chmod +x on ALL files). Nose does the
470 # right thing and it tries to avoid executables,
474 # right thing and it tries to avoid executables,
471 # setuptools unfortunately forces our hand here. This
475 # setuptools unfortunately forces our hand here. This
472 # has been discussed on the distutils list and the
476 # has been discussed on the distutils list and the
473 # setuptools devs refuse to fix this problem!
477 # setuptools devs refuse to fix this problem!
474 '--exe',
478 '--exe',
475 ]
479 ]
476 if '-a' not in argv and '-A' not in argv:
480 if '-a' not in argv and '-A' not in argv:
477 argv = argv + ['-a', '!crash']
481 argv = argv + ['-a', '!crash']
478
482
479 if nose.__version__ >= '0.11':
483 if nose.__version__ >= '0.11':
480 # I don't fully understand why we need this one, but depending on what
484 # I don't fully understand why we need this one, but depending on what
481 # directory the test suite is run from, if we don't give it, 0 tests
485 # directory the test suite is run from, if we don't give it, 0 tests
482 # get run. Specifically, if the test suite is run from the source dir
486 # get run. Specifically, if the test suite is run from the source dir
483 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
487 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
484 # even if the same call done in this directory works fine). It appears
488 # even if the same call done in this directory works fine). It appears
485 # that if the requested package is in the current dir, nose bails early
489 # that if the requested package is in the current dir, nose bails early
486 # by default. Since it's otherwise harmless, leave it in by default
490 # by default. Since it's otherwise harmless, leave it in by default
487 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
491 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
488 argv.append('--traverse-namespace')
492 argv.append('--traverse-namespace')
489
493
490 # use our plugin for doctesting. It will remove the standard doctest plugin
494 # use our plugin for doctesting. It will remove the standard doctest plugin
491 # if it finds it enabled
495 # if it finds it enabled
492 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
496 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
493 SubprocessStreamCapturePlugin() ]
497 SubprocessStreamCapturePlugin() ]
494
498
495 # Use working directory set by parent process (see iptestcontroller)
499 # Use working directory set by parent process (see iptestcontroller)
496 if 'IPTEST_WORKING_DIR' in os.environ:
500 if 'IPTEST_WORKING_DIR' in os.environ:
497 os.chdir(os.environ['IPTEST_WORKING_DIR'])
501 os.chdir(os.environ['IPTEST_WORKING_DIR'])
498
502
499 # We need a global ipython running in this process, but the special
503 # We need a global ipython running in this process, but the special
500 # in-process group spawns its own IPython kernels, so for *that* group we
504 # in-process group spawns its own IPython kernels, so for *that* group we
501 # must avoid also opening the global one (otherwise there's a conflict of
505 # must avoid also opening the global one (otherwise there's a conflict of
502 # singletons). Ultimately the solution to this problem is to refactor our
506 # singletons). Ultimately the solution to this problem is to refactor our
503 # assumptions about what needs to be a singleton and what doesn't (app
507 # assumptions about what needs to be a singleton and what doesn't (app
504 # objects should, individual shells shouldn't). But for now, this
508 # objects should, individual shells shouldn't). But for now, this
505 # workaround allows the test suite for the inprocess module to complete.
509 # workaround allows the test suite for the inprocess module to complete.
506 if 'kernel.inprocess' not in section.name:
510 if 'kernel.inprocess' not in section.name:
507 from IPython.testing import globalipapp
511 from IPython.testing import globalipapp
508 globalipapp.start_ipython()
512 globalipapp.start_ipython()
509
513
510 # Now nose can run
514 # Now nose can run
511 TestProgram(argv=argv, addplugins=plugins)
515 TestProgram(argv=argv, addplugins=plugins)
512
516
513 if __name__ == '__main__':
517 if __name__ == '__main__':
514 run_iptest()
518 run_iptest()
515
519
@@ -1,707 +1,709 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
2 """IPython Test Process Controller
3
3
4 This module runs one or more subprocesses which will actually run the IPython
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
5 test suite.
6
6
7 """
7 """
8
8
9 # Copyright (c) IPython Development Team.
9 # Copyright (c) IPython Development Team.
10 # Distributed under the terms of the Modified BSD License.
10 # Distributed under the terms of the Modified BSD License.
11
11
12 from __future__ import print_function
12 from __future__ import print_function
13
13
14 import argparse
14 import argparse
15 import json
15 import json
16 import multiprocessing.pool
16 import multiprocessing.pool
17 import os
17 import os
18 import re
18 import re
19 import requests
19 import requests
20 import shutil
20 import shutil
21 import signal
21 import signal
22 import sys
22 import sys
23 import subprocess
23 import subprocess
24 import time
24 import time
25
25
26 from .iptest import (
26 from .iptest import (
27 have, test_group_names as py_test_group_names, test_sections, StreamCapturer,
27 have, test_group_names as py_test_group_names, test_sections, StreamCapturer,
28 test_for,
28 test_for,
29 )
29 )
30 from IPython.utils.path import compress_user
30 from IPython.utils.path import compress_user
31 from IPython.utils.py3compat import bytes_to_str
31 from IPython.utils.py3compat import bytes_to_str
32 from IPython.utils.sysinfo import get_sys_info
32 from IPython.utils.sysinfo import get_sys_info
33 from IPython.utils.tempdir import TemporaryDirectory
33 from IPython.utils.tempdir import TemporaryDirectory
34 from IPython.utils.text import strip_ansi
34 from IPython.utils.text import strip_ansi
35
35
36 try:
36 try:
37 # Python >= 3.3
37 # Python >= 3.3
38 from subprocess import TimeoutExpired
38 from subprocess import TimeoutExpired
39 def popen_wait(p, timeout):
39 def popen_wait(p, timeout):
40 return p.wait(timeout)
40 return p.wait(timeout)
41 except ImportError:
41 except ImportError:
42 class TimeoutExpired(Exception):
42 class TimeoutExpired(Exception):
43 pass
43 pass
44 def popen_wait(p, timeout):
44 def popen_wait(p, timeout):
45 """backport of Popen.wait from Python 3"""
45 """backport of Popen.wait from Python 3"""
46 for i in range(int(10 * timeout)):
46 for i in range(int(10 * timeout)):
47 if p.poll() is not None:
47 if p.poll() is not None:
48 return
48 return
49 time.sleep(0.1)
49 time.sleep(0.1)
50 if p.poll() is None:
50 if p.poll() is None:
51 raise TimeoutExpired
51 raise TimeoutExpired
52
52
53 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
53 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
54
54
55 class TestController(object):
55 class TestController(object):
56 """Run tests in a subprocess
56 """Run tests in a subprocess
57 """
57 """
58 #: str, IPython test suite to be executed.
58 #: str, IPython test suite to be executed.
59 section = None
59 section = None
60 #: list, command line arguments to be executed
60 #: list, command line arguments to be executed
61 cmd = None
61 cmd = None
62 #: dict, extra environment variables to set for the subprocess
62 #: dict, extra environment variables to set for the subprocess
63 env = None
63 env = None
64 #: list, TemporaryDirectory instances to clear up when the process finishes
64 #: list, TemporaryDirectory instances to clear up when the process finishes
65 dirs = None
65 dirs = None
66 #: subprocess.Popen instance
66 #: subprocess.Popen instance
67 process = None
67 process = None
68 #: str, process stdout+stderr
68 #: str, process stdout+stderr
69 stdout = None
69 stdout = None
70
70
71 def __init__(self):
71 def __init__(self):
72 self.cmd = []
72 self.cmd = []
73 self.env = {}
73 self.env = {}
74 self.dirs = []
74 self.dirs = []
75
75
76 def setup(self):
76 def setup(self):
77 """Create temporary directories etc.
77 """Create temporary directories etc.
78
78
79 This is only called when we know the test group will be run. Things
79 This is only called when we know the test group will be run. Things
80 created here may be cleaned up by self.cleanup().
80 created here may be cleaned up by self.cleanup().
81 """
81 """
82 pass
82 pass
83
83
84 def launch(self, buffer_output=False):
84 def launch(self, buffer_output=False, capture_output=False):
85 # print('*** ENV:', self.env) # dbg
85 # print('*** ENV:', self.env) # dbg
86 # print('*** CMD:', self.cmd) # dbg
86 # print('*** CMD:', self.cmd) # dbg
87 env = os.environ.copy()
87 env = os.environ.copy()
88 env.update(self.env)
88 env.update(self.env)
89 output = subprocess.PIPE if buffer_output else None
89 if buffer_output:
90 stdout = subprocess.STDOUT if buffer_output else None
90 capture_output = True
91 self.process = subprocess.Popen(self.cmd, stdout=output,
91 self.stdout_capturer = c = StreamCapturer(echo=not buffer_output)
92 stderr=stdout, env=env)
92 c.start()
93 stdout = c.writefd if capture_output else None
94 stderr = subprocess.STDOUT if capture_output else None
95 self.process = subprocess.Popen(self.cmd, stdout=stdout,
96 stderr=stderr, env=env)
93
97
94 def wait(self):
98 def wait(self):
95 self.stdout, _ = self.process.communicate()
99 self.process.wait()
100 self.stdout_capturer.halt()
101 self.stdout = self.stdout_capturer.get_buffer()
96 return self.process.returncode
102 return self.process.returncode
97
103
98 def print_extra_info(self):
104 def print_extra_info(self):
99 """Print extra information about this test run.
105 """Print extra information about this test run.
100
106
101 If we're running in parallel and showing the concise view, this is only
107 If we're running in parallel and showing the concise view, this is only
102 called if the test group fails. Otherwise, it's called before the test
108 called if the test group fails. Otherwise, it's called before the test
103 group is started.
109 group is started.
104
110
105 The base implementation does nothing, but it can be overridden by
111 The base implementation does nothing, but it can be overridden by
106 subclasses.
112 subclasses.
107 """
113 """
108 return
114 return
109
115
110 def cleanup_process(self):
116 def cleanup_process(self):
111 """Cleanup on exit by killing any leftover processes."""
117 """Cleanup on exit by killing any leftover processes."""
112 subp = self.process
118 subp = self.process
113 if subp is None or (subp.poll() is not None):
119 if subp is None or (subp.poll() is not None):
114 return # Process doesn't exist, or is already dead.
120 return # Process doesn't exist, or is already dead.
115
121
116 try:
122 try:
117 print('Cleaning up stale PID: %d' % subp.pid)
123 print('Cleaning up stale PID: %d' % subp.pid)
118 subp.kill()
124 subp.kill()
119 except: # (OSError, WindowsError) ?
125 except: # (OSError, WindowsError) ?
120 # This is just a best effort, if we fail or the process was
126 # This is just a best effort, if we fail or the process was
121 # really gone, ignore it.
127 # really gone, ignore it.
122 pass
128 pass
123 else:
129 else:
124 for i in range(10):
130 for i in range(10):
125 if subp.poll() is None:
131 if subp.poll() is None:
126 time.sleep(0.1)
132 time.sleep(0.1)
127 else:
133 else:
128 break
134 break
129
135
130 if subp.poll() is None:
136 if subp.poll() is None:
131 # The process did not die...
137 # The process did not die...
132 print('... failed. Manual cleanup may be required.')
138 print('... failed. Manual cleanup may be required.')
133
139
134 def cleanup(self):
140 def cleanup(self):
135 "Kill process if it's still alive, and clean up temporary directories"
141 "Kill process if it's still alive, and clean up temporary directories"
136 self.cleanup_process()
142 self.cleanup_process()
137 for td in self.dirs:
143 for td in self.dirs:
138 td.cleanup()
144 td.cleanup()
139
145
140 __del__ = cleanup
146 __del__ = cleanup
141
147
142
148
143 class PyTestController(TestController):
149 class PyTestController(TestController):
144 """Run Python tests using IPython.testing.iptest"""
150 """Run Python tests using IPython.testing.iptest"""
145 #: str, Python command to execute in subprocess
151 #: str, Python command to execute in subprocess
146 pycmd = None
152 pycmd = None
147
153
148 def __init__(self, section, options):
154 def __init__(self, section, options):
149 """Create new test runner."""
155 """Create new test runner."""
150 TestController.__init__(self)
156 TestController.__init__(self)
151 self.section = section
157 self.section = section
152 # pycmd is put into cmd[2] in PyTestController.launch()
158 # pycmd is put into cmd[2] in PyTestController.launch()
153 self.cmd = [sys.executable, '-c', None, section]
159 self.cmd = [sys.executable, '-c', None, section]
154 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
160 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
155 self.options = options
161 self.options = options
156
162
157 def setup(self):
163 def setup(self):
158 ipydir = TemporaryDirectory()
164 ipydir = TemporaryDirectory()
159 self.dirs.append(ipydir)
165 self.dirs.append(ipydir)
160 self.env['IPYTHONDIR'] = ipydir.name
166 self.env['IPYTHONDIR'] = ipydir.name
161 self.workingdir = workingdir = TemporaryDirectory()
167 self.workingdir = workingdir = TemporaryDirectory()
162 self.dirs.append(workingdir)
168 self.dirs.append(workingdir)
163 self.env['IPTEST_WORKING_DIR'] = workingdir.name
169 self.env['IPTEST_WORKING_DIR'] = workingdir.name
164 # This means we won't get odd effects from our own matplotlib config
170 # This means we won't get odd effects from our own matplotlib config
165 self.env['MPLCONFIGDIR'] = workingdir.name
171 self.env['MPLCONFIGDIR'] = workingdir.name
166
172
167 # From options:
173 # From options:
168 if self.options.xunit:
174 if self.options.xunit:
169 self.add_xunit()
175 self.add_xunit()
170 if self.options.coverage:
176 if self.options.coverage:
171 self.add_coverage()
177 self.add_coverage()
172 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
178 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
173 self.cmd.extend(self.options.extra_args)
179 self.cmd.extend(self.options.extra_args)
174
180
175 @property
181 @property
176 def will_run(self):
182 def will_run(self):
177 try:
183 try:
178 return test_sections[self.section].will_run
184 return test_sections[self.section].will_run
179 except KeyError:
185 except KeyError:
180 return True
186 return True
181
187
182 def add_xunit(self):
188 def add_xunit(self):
183 xunit_file = os.path.abspath(self.section + '.xunit.xml')
189 xunit_file = os.path.abspath(self.section + '.xunit.xml')
184 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
190 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
185
191
186 def add_coverage(self):
192 def add_coverage(self):
187 try:
193 try:
188 sources = test_sections[self.section].includes
194 sources = test_sections[self.section].includes
189 except KeyError:
195 except KeyError:
190 sources = ['IPython']
196 sources = ['IPython']
191
197
192 coverage_rc = ("[run]\n"
198 coverage_rc = ("[run]\n"
193 "data_file = {data_file}\n"
199 "data_file = {data_file}\n"
194 "source =\n"
200 "source =\n"
195 " {source}\n"
201 " {source}\n"
196 ).format(data_file=os.path.abspath('.coverage.'+self.section),
202 ).format(data_file=os.path.abspath('.coverage.'+self.section),
197 source="\n ".join(sources))
203 source="\n ".join(sources))
198 config_file = os.path.join(self.workingdir.name, '.coveragerc')
204 config_file = os.path.join(self.workingdir.name, '.coveragerc')
199 with open(config_file, 'w') as f:
205 with open(config_file, 'w') as f:
200 f.write(coverage_rc)
206 f.write(coverage_rc)
201
207
202 self.env['COVERAGE_PROCESS_START'] = config_file
208 self.env['COVERAGE_PROCESS_START'] = config_file
203 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
209 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
204
210
205 def launch(self, buffer_output=False):
211 def launch(self, buffer_output=False):
206 self.cmd[2] = self.pycmd
212 self.cmd[2] = self.pycmd
207 super(PyTestController, self).launch(buffer_output=buffer_output)
213 super(PyTestController, self).launch(buffer_output=buffer_output)
208
214
209
215
210 js_prefix = 'js/'
216 js_prefix = 'js/'
211
217
212 def get_js_test_dir():
218 def get_js_test_dir():
213 import IPython.html.tests as t
219 import IPython.html.tests as t
214 return os.path.join(os.path.dirname(t.__file__), '')
220 return os.path.join(os.path.dirname(t.__file__), '')
215
221
216 def all_js_groups():
222 def all_js_groups():
217 import glob
223 import glob
218 test_dir = get_js_test_dir()
224 test_dir = get_js_test_dir()
219 all_subdirs = glob.glob(test_dir + '[!_]*/')
225 all_subdirs = glob.glob(test_dir + '[!_]*/')
220 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
226 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
221
227
222 class JSController(TestController):
228 class JSController(TestController):
223 """Run CasperJS tests """
229 """Run CasperJS tests """
224
230
225 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3',
231 requirements = ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3',
226 'jsonschema']
232 'jsonschema']
227 display_slimer_output = False
228
233
229 def __init__(self, section, xunit=True, engine='phantomjs', url=None):
234 def __init__(self, section, xunit=True, engine='phantomjs', url=None):
230 """Create new test runner."""
235 """Create new test runner."""
231 TestController.__init__(self)
236 TestController.__init__(self)
232 self.engine = engine
237 self.engine = engine
233 self.section = section
238 self.section = section
234 self.xunit = xunit
239 self.xunit = xunit
235 self.url = url
240 self.url = url
236 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
241 self.slimer_failure = re.compile('^FAIL.*', flags=re.MULTILINE)
237 js_test_dir = get_js_test_dir()
242 js_test_dir = get_js_test_dir()
238 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
243 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
239 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
244 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
240 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
245 self.cmd = ['casperjs', 'test', includes, test_cases, '--engine=%s' % self.engine]
241
246
242 def setup(self):
247 def setup(self):
243 self.ipydir = TemporaryDirectory()
248 self.ipydir = TemporaryDirectory()
244 self.nbdir = TemporaryDirectory()
249 self.nbdir = TemporaryDirectory()
245 self.dirs.append(self.ipydir)
250 self.dirs.append(self.ipydir)
246 self.dirs.append(self.nbdir)
251 self.dirs.append(self.nbdir)
247 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
252 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
248 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
253 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
249
254
250 if self.xunit:
255 if self.xunit:
251 self.add_xunit()
256 self.add_xunit()
252
257
253 # If a url was specified, use that for the testing.
258 # If a url was specified, use that for the testing.
254 if self.url:
259 if self.url:
255 try:
260 try:
256 alive = requests.get(self.url).status_code == 200
261 alive = requests.get(self.url).status_code == 200
257 except:
262 except:
258 alive = False
263 alive = False
259
264
260 if alive:
265 if alive:
261 self.cmd.append("--url=%s" % self.url)
266 self.cmd.append("--url=%s" % self.url)
262 else:
267 else:
263 raise Exception('Could not reach "%s".' % self.url)
268 raise Exception('Could not reach "%s".' % self.url)
264 else:
269 else:
265 # start the ipython notebook, so we get the port number
270 # start the ipython notebook, so we get the port number
266 self.server_port = 0
271 self.server_port = 0
267 self._init_server()
272 self._init_server()
268 if self.server_port:
273 if self.server_port:
269 self.cmd.append("--port=%i" % self.server_port)
274 self.cmd.append("--port=%i" % self.server_port)
270 else:
275 else:
271 # don't launch tests if the server didn't start
276 # don't launch tests if the server didn't start
272 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
277 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
273
278
274 def add_xunit(self):
279 def add_xunit(self):
275 xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
280 xunit_file = os.path.abspath(self.section.replace('/','.') + '.xunit.xml')
276 self.cmd.append('--xunit=%s' % xunit_file)
281 self.cmd.append('--xunit=%s' % xunit_file)
277
282
278 def launch(self, buffer_output):
283 def launch(self, buffer_output):
279 # If the engine is SlimerJS, we need to buffer the output because
284 # If the engine is SlimerJS, we need to buffer the output because
280 # SlimerJS does not support exit codes, so CasperJS always returns 0.
285 # SlimerJS does not support exit codes, so CasperJS always returns 0.
281 if self.engine == 'slimerjs' and not buffer_output:
286 if self.engine == 'slimerjs' and not buffer_output:
282 self.display_slimer_output = True
287 return super(JSController, self).launch(capture_output=True)
283 return super(JSController, self).launch(buffer_output=True)
284
288
285 else:
289 else:
286 return super(JSController, self).launch(buffer_output=buffer_output)
290 return super(JSController, self).launch(buffer_output=buffer_output)
287
291
288 def wait(self, *pargs, **kwargs):
292 def wait(self, *pargs, **kwargs):
289 """Wait for the JSController to finish"""
293 """Wait for the JSController to finish"""
290 ret = super(JSController, self).wait(*pargs, **kwargs)
294 ret = super(JSController, self).wait(*pargs, **kwargs)
291 # If this is a SlimerJS controller, check the captured stdout for
295 # If this is a SlimerJS controller, check the captured stdout for
292 # errors. Otherwise, just return the return code.
296 # errors. Otherwise, just return the return code.
293 if self.engine == 'slimerjs':
297 if self.engine == 'slimerjs':
294 stdout = bytes_to_str(self.stdout)
298 stdout = bytes_to_str(self.stdout)
295 if self.display_slimer_output:
296 print(stdout)
297 if ret != 0:
299 if ret != 0:
298 # This could still happen e.g. if it's stopped by SIGINT
300 # This could still happen e.g. if it's stopped by SIGINT
299 return ret
301 return ret
300 return bool(self.slimer_failure.search(strip_ansi(stdout)))
302 return bool(self.slimer_failure.search(strip_ansi(stdout)))
301 else:
303 else:
302 return ret
304 return ret
303
305
304 def print_extra_info(self):
306 def print_extra_info(self):
305 print("Running tests with notebook directory %r" % self.nbdir.name)
307 print("Running tests with notebook directory %r" % self.nbdir.name)
306
308
307 @property
309 @property
308 def will_run(self):
310 def will_run(self):
309 should_run = all(have[a] for a in self.requirements + [self.engine])
311 should_run = all(have[a] for a in self.requirements + [self.engine])
310 tornado4 = test_for('tornado.version_info', (4,0,0), callback=None)
312 tornado4 = test_for('tornado.version_info', (4,0,0), callback=None)
311 if should_run and self.engine == 'phantomjs' and tornado4:
313 if should_run and self.engine == 'phantomjs' and tornado4:
312 print("phantomjs cannot connect websockets to tornado 4", file=sys.stderr)
314 print("phantomjs cannot connect websockets to tornado 4", file=sys.stderr)
313 return False
315 return False
314 return should_run
316 return should_run
315
317
316 def _init_server(self):
318 def _init_server(self):
317 "Start the notebook server in a separate process"
319 "Start the notebook server in a separate process"
318 self.server_command = command = [sys.executable,
320 self.server_command = command = [sys.executable,
319 '-m', 'IPython.html',
321 '-m', 'IPython.html',
320 '--no-browser',
322 '--no-browser',
321 '--ipython-dir', self.ipydir.name,
323 '--ipython-dir', self.ipydir.name,
322 '--notebook-dir', self.nbdir.name,
324 '--notebook-dir', self.nbdir.name,
323 ]
325 ]
324 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
326 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
325 # which run afoul of ipc's maximum path length.
327 # which run afoul of ipc's maximum path length.
326 if sys.platform.startswith('linux'):
328 if sys.platform.startswith('linux'):
327 command.append('--KernelManager.transport=ipc')
329 command.append('--KernelManager.transport=ipc')
328 self.stream_capturer = c = StreamCapturer()
330 self.stream_capturer = c = StreamCapturer()
329 c.start()
331 c.start()
330 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT, cwd=self.nbdir.name)
332 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT, cwd=self.nbdir.name)
331 self.server_info_file = os.path.join(self.ipydir.name,
333 self.server_info_file = os.path.join(self.ipydir.name,
332 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
334 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
333 )
335 )
334 self._wait_for_server()
336 self._wait_for_server()
335
337
336 def _wait_for_server(self):
338 def _wait_for_server(self):
337 """Wait 30 seconds for the notebook server to start"""
339 """Wait 30 seconds for the notebook server to start"""
338 for i in range(300):
340 for i in range(300):
339 if self.server.poll() is not None:
341 if self.server.poll() is not None:
340 return self._failed_to_start()
342 return self._failed_to_start()
341 if os.path.exists(self.server_info_file):
343 if os.path.exists(self.server_info_file):
342 try:
344 try:
343 self._load_server_info()
345 self._load_server_info()
344 except ValueError:
346 except ValueError:
345 # If the server is halfway through writing the file, we may
347 # If the server is halfway through writing the file, we may
346 # get invalid JSON; it should be ready next iteration.
348 # get invalid JSON; it should be ready next iteration.
347 pass
349 pass
348 else:
350 else:
349 return
351 return
350 time.sleep(0.1)
352 time.sleep(0.1)
351 print("Notebook server-info file never arrived: %s" % self.server_info_file,
353 print("Notebook server-info file never arrived: %s" % self.server_info_file,
352 file=sys.stderr
354 file=sys.stderr
353 )
355 )
354
356
355 def _failed_to_start(self):
357 def _failed_to_start(self):
356 """Notebook server exited prematurely"""
358 """Notebook server exited prematurely"""
357 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
359 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
358 print("Notebook failed to start: ", file=sys.stderr)
360 print("Notebook failed to start: ", file=sys.stderr)
359 print(self.server_command)
361 print(self.server_command)
360 print(captured, file=sys.stderr)
362 print(captured, file=sys.stderr)
361
363
362 def _load_server_info(self):
364 def _load_server_info(self):
363 """Notebook server started, load connection info from JSON"""
365 """Notebook server started, load connection info from JSON"""
364 with open(self.server_info_file) as f:
366 with open(self.server_info_file) as f:
365 info = json.load(f)
367 info = json.load(f)
366 self.server_port = info['port']
368 self.server_port = info['port']
367
369
368 def cleanup(self):
370 def cleanup(self):
369 try:
371 try:
370 self.server.terminate()
372 self.server.terminate()
371 except OSError:
373 except OSError:
372 # already dead
374 # already dead
373 pass
375 pass
374 # wait 10s for the server to shutdown
376 # wait 10s for the server to shutdown
375 try:
377 try:
376 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
378 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
377 except TimeoutExpired:
379 except TimeoutExpired:
378 # server didn't terminate, kill it
380 # server didn't terminate, kill it
379 try:
381 try:
380 print("Failed to terminate notebook server, killing it.",
382 print("Failed to terminate notebook server, killing it.",
381 file=sys.stderr
383 file=sys.stderr
382 )
384 )
383 self.server.kill()
385 self.server.kill()
384 except OSError:
386 except OSError:
385 # already dead
387 # already dead
386 pass
388 pass
387 # wait another 10s
389 # wait another 10s
388 try:
390 try:
389 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
391 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
390 except TimeoutExpired:
392 except TimeoutExpired:
391 print("Notebook server still running (%s)" % self.server_info_file,
393 print("Notebook server still running (%s)" % self.server_info_file,
392 file=sys.stderr
394 file=sys.stderr
393 )
395 )
394
396
395 self.stream_capturer.halt()
397 self.stream_capturer.halt()
396 TestController.cleanup(self)
398 TestController.cleanup(self)
397
399
398
400
399 def prepare_controllers(options):
401 def prepare_controllers(options):
400 """Returns two lists of TestController instances, those to run, and those
402 """Returns two lists of TestController instances, those to run, and those
401 not to run."""
403 not to run."""
402 testgroups = options.testgroups
404 testgroups = options.testgroups
403 if testgroups:
405 if testgroups:
404 if 'js' in testgroups:
406 if 'js' in testgroups:
405 js_testgroups = all_js_groups()
407 js_testgroups = all_js_groups()
406 else:
408 else:
407 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
409 js_testgroups = [g for g in testgroups if g.startswith(js_prefix)]
408
410
409 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
411 py_testgroups = [g for g in testgroups if g not in ['js'] + js_testgroups]
410 else:
412 else:
411 py_testgroups = py_test_group_names
413 py_testgroups = py_test_group_names
412 if not options.all:
414 if not options.all:
413 js_testgroups = []
415 js_testgroups = []
414 test_sections['parallel'].enabled = False
416 test_sections['parallel'].enabled = False
415 else:
417 else:
416 js_testgroups = all_js_groups()
418 js_testgroups = all_js_groups()
417
419
418 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
420 engine = 'slimerjs' if options.slimerjs else 'phantomjs'
419 c_js = [JSController(name, xunit=options.xunit, engine=engine, url=options.url) for name in js_testgroups]
421 c_js = [JSController(name, xunit=options.xunit, engine=engine, url=options.url) for name in js_testgroups]
420 c_py = [PyTestController(name, options) for name in py_testgroups]
422 c_py = [PyTestController(name, options) for name in py_testgroups]
421
423
422 controllers = c_py + c_js
424 controllers = c_py + c_js
423 to_run = [c for c in controllers if c.will_run]
425 to_run = [c for c in controllers if c.will_run]
424 not_run = [c for c in controllers if not c.will_run]
426 not_run = [c for c in controllers if not c.will_run]
425 return to_run, not_run
427 return to_run, not_run
426
428
427 def do_run(controller, buffer_output=True):
429 def do_run(controller, buffer_output=True):
428 """Setup and run a test controller.
430 """Setup and run a test controller.
429
431
430 If buffer_output is True, no output is displayed, to avoid it appearing
432 If buffer_output is True, no output is displayed, to avoid it appearing
431 interleaved. In this case, the caller is responsible for displaying test
433 interleaved. In this case, the caller is responsible for displaying test
432 output on failure.
434 output on failure.
433
435
434 Returns
436 Returns
435 -------
437 -------
436 controller : TestController
438 controller : TestController
437 The same controller as passed in, as a convenience for using map() type
439 The same controller as passed in, as a convenience for using map() type
438 APIs.
440 APIs.
439 exitcode : int
441 exitcode : int
440 The exit code of the test subprocess. Non-zero indicates failure.
442 The exit code of the test subprocess. Non-zero indicates failure.
441 """
443 """
442 try:
444 try:
443 try:
445 try:
444 controller.setup()
446 controller.setup()
445 if not buffer_output:
447 if not buffer_output:
446 controller.print_extra_info()
448 controller.print_extra_info()
447 controller.launch(buffer_output=buffer_output)
449 controller.launch(buffer_output=buffer_output)
448 except Exception:
450 except Exception:
449 import traceback
451 import traceback
450 traceback.print_exc()
452 traceback.print_exc()
451 return controller, 1 # signal failure
453 return controller, 1 # signal failure
452
454
453 exitcode = controller.wait()
455 exitcode = controller.wait()
454 return controller, exitcode
456 return controller, exitcode
455
457
456 except KeyboardInterrupt:
458 except KeyboardInterrupt:
457 return controller, -signal.SIGINT
459 return controller, -signal.SIGINT
458 finally:
460 finally:
459 controller.cleanup()
461 controller.cleanup()
460
462
461 def report():
463 def report():
462 """Return a string with a summary report of test-related variables."""
464 """Return a string with a summary report of test-related variables."""
463 inf = get_sys_info()
465 inf = get_sys_info()
464 out = []
466 out = []
465 def _add(name, value):
467 def _add(name, value):
466 out.append((name, value))
468 out.append((name, value))
467
469
468 _add('IPython version', inf['ipython_version'])
470 _add('IPython version', inf['ipython_version'])
469 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
471 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
470 _add('IPython package', compress_user(inf['ipython_path']))
472 _add('IPython package', compress_user(inf['ipython_path']))
471 _add('Python version', inf['sys_version'].replace('\n',''))
473 _add('Python version', inf['sys_version'].replace('\n',''))
472 _add('sys.executable', compress_user(inf['sys_executable']))
474 _add('sys.executable', compress_user(inf['sys_executable']))
473 _add('Platform', inf['platform'])
475 _add('Platform', inf['platform'])
474
476
475 width = max(len(n) for (n,v) in out)
477 width = max(len(n) for (n,v) in out)
476 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
478 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
477
479
478 avail = []
480 avail = []
479 not_avail = []
481 not_avail = []
480
482
481 for k, is_avail in have.items():
483 for k, is_avail in have.items():
482 if is_avail:
484 if is_avail:
483 avail.append(k)
485 avail.append(k)
484 else:
486 else:
485 not_avail.append(k)
487 not_avail.append(k)
486
488
487 if avail:
489 if avail:
488 out.append('\nTools and libraries available at test time:\n')
490 out.append('\nTools and libraries available at test time:\n')
489 avail.sort()
491 avail.sort()
490 out.append(' ' + ' '.join(avail)+'\n')
492 out.append(' ' + ' '.join(avail)+'\n')
491
493
492 if not_avail:
494 if not_avail:
493 out.append('\nTools and libraries NOT available at test time:\n')
495 out.append('\nTools and libraries NOT available at test time:\n')
494 not_avail.sort()
496 not_avail.sort()
495 out.append(' ' + ' '.join(not_avail)+'\n')
497 out.append(' ' + ' '.join(not_avail)+'\n')
496
498
497 return ''.join(out)
499 return ''.join(out)
498
500
499 def run_iptestall(options):
501 def run_iptestall(options):
500 """Run the entire IPython test suite by calling nose and trial.
502 """Run the entire IPython test suite by calling nose and trial.
501
503
502 This function constructs :class:`IPTester` instances for all IPython
504 This function constructs :class:`IPTester` instances for all IPython
503 modules and package and then runs each of them. This causes the modules
505 modules and package and then runs each of them. This causes the modules
504 and packages of IPython to be tested each in their own subprocess using
506 and packages of IPython to be tested each in their own subprocess using
505 nose.
507 nose.
506
508
507 Parameters
509 Parameters
508 ----------
510 ----------
509
511
510 All parameters are passed as attributes of the options object.
512 All parameters are passed as attributes of the options object.
511
513
512 testgroups : list of str
514 testgroups : list of str
513 Run only these sections of the test suite. If empty, run all the available
515 Run only these sections of the test suite. If empty, run all the available
514 sections.
516 sections.
515
517
516 fast : int or None
518 fast : int or None
517 Run the test suite in parallel, using n simultaneous processes. If None
519 Run the test suite in parallel, using n simultaneous processes. If None
518 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
520 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
519
521
520 inc_slow : bool
522 inc_slow : bool
521 Include slow tests, like IPython.parallel. By default, these tests aren't
523 Include slow tests, like IPython.parallel. By default, these tests aren't
522 run.
524 run.
523
525
524 slimerjs : bool
526 slimerjs : bool
525 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
527 Use slimerjs if it's installed instead of phantomjs for casperjs tests.
526
528
527 url : unicode
529 url : unicode
528 Address:port to use when running the JS tests.
530 Address:port to use when running the JS tests.
529
531
530 xunit : bool
532 xunit : bool
531 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
533 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
532
534
533 coverage : bool or str
535 coverage : bool or str
534 Measure code coverage from tests. True will store the raw coverage data,
536 Measure code coverage from tests. True will store the raw coverage data,
535 or pass 'html' or 'xml' to get reports.
537 or pass 'html' or 'xml' to get reports.
536
538
537 extra_args : list
539 extra_args : list
538 Extra arguments to pass to the test subprocesses, e.g. '-v'
540 Extra arguments to pass to the test subprocesses, e.g. '-v'
539 """
541 """
540 to_run, not_run = prepare_controllers(options)
542 to_run, not_run = prepare_controllers(options)
541
543
542 def justify(ltext, rtext, width=70, fill='-'):
544 def justify(ltext, rtext, width=70, fill='-'):
543 ltext += ' '
545 ltext += ' '
544 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
546 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
545 return ltext + rtext
547 return ltext + rtext
546
548
547 # Run all test runners, tracking execution time
549 # Run all test runners, tracking execution time
548 failed = []
550 failed = []
549 t_start = time.time()
551 t_start = time.time()
550
552
551 print()
553 print()
552 if options.fast == 1:
554 if options.fast == 1:
553 # This actually means sequential, i.e. with 1 job
555 # This actually means sequential, i.e. with 1 job
554 for controller in to_run:
556 for controller in to_run:
555 print('Test group:', controller.section)
557 print('Test group:', controller.section)
556 sys.stdout.flush() # Show in correct order when output is piped
558 sys.stdout.flush() # Show in correct order when output is piped
557 controller, res = do_run(controller, buffer_output=False)
559 controller, res = do_run(controller, buffer_output=False)
558 if res:
560 if res:
559 failed.append(controller)
561 failed.append(controller)
560 if res == -signal.SIGINT:
562 if res == -signal.SIGINT:
561 print("Interrupted")
563 print("Interrupted")
562 break
564 break
563 print()
565 print()
564
566
565 else:
567 else:
566 # Run tests concurrently
568 # Run tests concurrently
567 try:
569 try:
568 pool = multiprocessing.pool.ThreadPool(options.fast)
570 pool = multiprocessing.pool.ThreadPool(options.fast)
569 for (controller, res) in pool.imap_unordered(do_run, to_run):
571 for (controller, res) in pool.imap_unordered(do_run, to_run):
570 res_string = 'OK' if res == 0 else 'FAILED'
572 res_string = 'OK' if res == 0 else 'FAILED'
571 print(justify('Test group: ' + controller.section, res_string))
573 print(justify('Test group: ' + controller.section, res_string))
572 if res:
574 if res:
573 controller.print_extra_info()
575 controller.print_extra_info()
574 print(bytes_to_str(controller.stdout))
576 print(bytes_to_str(controller.stdout))
575 failed.append(controller)
577 failed.append(controller)
576 if res == -signal.SIGINT:
578 if res == -signal.SIGINT:
577 print("Interrupted")
579 print("Interrupted")
578 break
580 break
579 except KeyboardInterrupt:
581 except KeyboardInterrupt:
580 return
582 return
581
583
582 for controller in not_run:
584 for controller in not_run:
583 print(justify('Test group: ' + controller.section, 'NOT RUN'))
585 print(justify('Test group: ' + controller.section, 'NOT RUN'))
584
586
585 t_end = time.time()
587 t_end = time.time()
586 t_tests = t_end - t_start
588 t_tests = t_end - t_start
587 nrunners = len(to_run)
589 nrunners = len(to_run)
588 nfail = len(failed)
590 nfail = len(failed)
589 # summarize results
591 # summarize results
590 print('_'*70)
592 print('_'*70)
591 print('Test suite completed for system with the following information:')
593 print('Test suite completed for system with the following information:')
592 print(report())
594 print(report())
593 took = "Took %.3fs." % t_tests
595 took = "Took %.3fs." % t_tests
594 print('Status: ', end='')
596 print('Status: ', end='')
595 if not failed:
597 if not failed:
596 print('OK (%d test groups).' % nrunners, took)
598 print('OK (%d test groups).' % nrunners, took)
597 else:
599 else:
598 # If anything went wrong, point out what command to rerun manually to
600 # If anything went wrong, point out what command to rerun manually to
599 # see the actual errors and individual summary
601 # see the actual errors and individual summary
600 failed_sections = [c.section for c in failed]
602 failed_sections = [c.section for c in failed]
601 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
603 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
602 nrunners, ', '.join(failed_sections)), took)
604 nrunners, ', '.join(failed_sections)), took)
603 print()
605 print()
604 print('You may wish to rerun these, with:')
606 print('You may wish to rerun these, with:')
605 print(' iptest', *failed_sections)
607 print(' iptest', *failed_sections)
606 print()
608 print()
607
609
608 if options.coverage:
610 if options.coverage:
609 from coverage import coverage
611 from coverage import coverage
610 cov = coverage(data_file='.coverage')
612 cov = coverage(data_file='.coverage')
611 cov.combine()
613 cov.combine()
612 cov.save()
614 cov.save()
613
615
614 # Coverage HTML report
616 # Coverage HTML report
615 if options.coverage == 'html':
617 if options.coverage == 'html':
616 html_dir = 'ipy_htmlcov'
618 html_dir = 'ipy_htmlcov'
617 shutil.rmtree(html_dir, ignore_errors=True)
619 shutil.rmtree(html_dir, ignore_errors=True)
618 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
620 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
619 sys.stdout.flush()
621 sys.stdout.flush()
620
622
621 # Custom HTML reporter to clean up module names.
623 # Custom HTML reporter to clean up module names.
622 from coverage.html import HtmlReporter
624 from coverage.html import HtmlReporter
623 class CustomHtmlReporter(HtmlReporter):
625 class CustomHtmlReporter(HtmlReporter):
624 def find_code_units(self, morfs):
626 def find_code_units(self, morfs):
625 super(CustomHtmlReporter, self).find_code_units(morfs)
627 super(CustomHtmlReporter, self).find_code_units(morfs)
626 for cu in self.code_units:
628 for cu in self.code_units:
627 nameparts = cu.name.split(os.sep)
629 nameparts = cu.name.split(os.sep)
628 if 'IPython' not in nameparts:
630 if 'IPython' not in nameparts:
629 continue
631 continue
630 ix = nameparts.index('IPython')
632 ix = nameparts.index('IPython')
631 cu.name = '.'.join(nameparts[ix:])
633 cu.name = '.'.join(nameparts[ix:])
632
634
633 # Reimplement the html_report method with our custom reporter
635 # Reimplement the html_report method with our custom reporter
634 cov._harvest_data()
636 cov._harvest_data()
635 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
637 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
636 html_title='IPython test coverage',
638 html_title='IPython test coverage',
637 )
639 )
638 reporter = CustomHtmlReporter(cov, cov.config)
640 reporter = CustomHtmlReporter(cov, cov.config)
639 reporter.report(None)
641 reporter.report(None)
640 print('done.')
642 print('done.')
641
643
642 # Coverage XML report
644 # Coverage XML report
643 elif options.coverage == 'xml':
645 elif options.coverage == 'xml':
644 cov.xml_report(outfile='ipy_coverage.xml')
646 cov.xml_report(outfile='ipy_coverage.xml')
645
647
646 if failed:
648 if failed:
647 # Ensure that our exit code indicates failure
649 # Ensure that our exit code indicates failure
648 sys.exit(1)
650 sys.exit(1)
649
651
650 argparser = argparse.ArgumentParser(description='Run IPython test suite')
652 argparser = argparse.ArgumentParser(description='Run IPython test suite')
651 argparser.add_argument('testgroups', nargs='*',
653 argparser.add_argument('testgroups', nargs='*',
652 help='Run specified groups of tests. If omitted, run '
654 help='Run specified groups of tests. If omitted, run '
653 'all tests.')
655 'all tests.')
654 argparser.add_argument('--all', action='store_true',
656 argparser.add_argument('--all', action='store_true',
655 help='Include slow tests not run by default.')
657 help='Include slow tests not run by default.')
656 argparser.add_argument('--slimerjs', action='store_true',
658 argparser.add_argument('--slimerjs', action='store_true',
657 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
659 help="Use slimerjs if it's installed instead of phantomjs for casperjs tests.")
658 argparser.add_argument('--url', help="URL to use for the JS tests.")
660 argparser.add_argument('--url', help="URL to use for the JS tests.")
659 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
661 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
660 help='Run test sections in parallel. This starts as many '
662 help='Run test sections in parallel. This starts as many '
661 'processes as you have cores, or you can specify a number.')
663 'processes as you have cores, or you can specify a number.')
662 argparser.add_argument('--xunit', action='store_true',
664 argparser.add_argument('--xunit', action='store_true',
663 help='Produce Xunit XML results')
665 help='Produce Xunit XML results')
664 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
666 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
665 help="Measure test coverage. Specify 'html' or "
667 help="Measure test coverage. Specify 'html' or "
666 "'xml' to get reports.")
668 "'xml' to get reports.")
667 argparser.add_argument('--subproc-streams', default='capture',
669 argparser.add_argument('--subproc-streams', default='capture',
668 help="What to do with stdout/stderr from subprocesses. "
670 help="What to do with stdout/stderr from subprocesses. "
669 "'capture' (default), 'show' and 'discard' are the options.")
671 "'capture' (default), 'show' and 'discard' are the options.")
670
672
671 def default_options():
673 def default_options():
672 """Get an argparse Namespace object with the default arguments, to pass to
674 """Get an argparse Namespace object with the default arguments, to pass to
673 :func:`run_iptestall`.
675 :func:`run_iptestall`.
674 """
676 """
675 options = argparser.parse_args([])
677 options = argparser.parse_args([])
676 options.extra_args = []
678 options.extra_args = []
677 return options
679 return options
678
680
679 def main():
681 def main():
680 # iptest doesn't work correctly if the working directory is the
682 # iptest doesn't work correctly if the working directory is the
681 # root of the IPython source tree. Tell the user to avoid
683 # root of the IPython source tree. Tell the user to avoid
682 # frustration.
684 # frustration.
683 if os.path.exists(os.path.join(os.getcwd(),
685 if os.path.exists(os.path.join(os.getcwd(),
684 'IPython', 'testing', '__main__.py')):
686 'IPython', 'testing', '__main__.py')):
685 print("Don't run iptest from the IPython source directory",
687 print("Don't run iptest from the IPython source directory",
686 file=sys.stderr)
688 file=sys.stderr)
687 sys.exit(1)
689 sys.exit(1)
688 # Arguments after -- should be passed through to nose. Argparse treats
690 # Arguments after -- should be passed through to nose. Argparse treats
689 # everything after -- as regular positional arguments, so we separate them
691 # everything after -- as regular positional arguments, so we separate them
690 # first.
692 # first.
691 try:
693 try:
692 ix = sys.argv.index('--')
694 ix = sys.argv.index('--')
693 except ValueError:
695 except ValueError:
694 to_parse = sys.argv[1:]
696 to_parse = sys.argv[1:]
695 extra_args = []
697 extra_args = []
696 else:
698 else:
697 to_parse = sys.argv[1:ix]
699 to_parse = sys.argv[1:ix]
698 extra_args = sys.argv[ix+1:]
700 extra_args = sys.argv[ix+1:]
699
701
700 options = argparser.parse_args(to_parse)
702 options = argparser.parse_args(to_parse)
701 options.extra_args = extra_args
703 options.extra_args = extra_args
702
704
703 run_iptestall(options)
705 run_iptestall(options)
704
706
705
707
706 if __name__ == '__main__':
708 if __name__ == '__main__':
707 main()
709 main()
General Comments 0
You need to be logged in to leave comments. Login now