##// END OF EJS Templates
Separate out machinery for running JS tests
Thomas Kluyver -
Show More
@@ -1,530 +1,527 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Suite Runner.
2 """IPython Test Suite Runner.
3
3
4 This module provides a main entry point to a user script to test IPython
4 This module provides a main entry point to a user script to test IPython
5 itself from the command line. There are two ways of running this script:
5 itself from the command line. There are two ways of running this script:
6
6
7 1. With the syntax `iptest all`. This runs our entire test suite by
7 1. With the syntax `iptest all`. This runs our entire test suite by
8 calling this script (with different arguments) recursively. This
8 calling this script (with different arguments) recursively. This
9 causes modules and package to be tested in different processes, using nose
9 causes modules and package to be tested in different processes, using nose
10 or trial where appropriate.
10 or trial where appropriate.
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 the script simply calls nose, but with special command line flags and
12 the script simply calls nose, but with special command line flags and
13 plugins loaded.
13 plugins loaded.
14
14
15 """
15 """
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Copyright (C) 2009-2011 The IPython Development Team
18 # Copyright (C) 2009-2011 The IPython Development Team
19 #
19 #
20 # Distributed under the terms of the BSD License. The full license is in
20 # Distributed under the terms of the BSD License. The full license is in
21 # the file COPYING, distributed as part of this software.
21 # the file COPYING, distributed as part of this software.
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Imports
25 # Imports
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 from __future__ import print_function
27 from __future__ import print_function
28
28
29 # Stdlib
29 # Stdlib
30 import glob
30 import glob
31 from io import BytesIO
31 from io import BytesIO
32 import os
32 import os
33 import os.path as path
33 import os.path as path
34 from select import select
34 from select import select
35 import sys
35 import sys
36 from threading import Thread, Lock, Event
36 from threading import Thread, Lock, Event
37 import warnings
37 import warnings
38
38
39 # Now, proceed to import nose itself
39 # Now, proceed to import nose itself
40 import nose.plugins.builtin
40 import nose.plugins.builtin
41 from nose.plugins.xunit import Xunit
41 from nose.plugins.xunit import Xunit
42 from nose import SkipTest
42 from nose import SkipTest
43 from nose.core import TestProgram
43 from nose.core import TestProgram
44 from nose.plugins import Plugin
44 from nose.plugins import Plugin
45 from nose.util import safe_str
45 from nose.util import safe_str
46
46
47 # Our own imports
47 # Our own imports
48 from IPython.utils.process import is_cmd_found
48 from IPython.utils.process import is_cmd_found
49 from IPython.utils.importstring import import_item
49 from IPython.utils.importstring import import_item
50 from IPython.testing.plugin.ipdoctest import IPythonDoctest
50 from IPython.testing.plugin.ipdoctest import IPythonDoctest
51 from IPython.external.decorators import KnownFailure, knownfailureif
51 from IPython.external.decorators import KnownFailure, knownfailureif
52
52
53 pjoin = path.join
53 pjoin = path.join
54
54
55
55
56 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
57 # Globals
57 # Globals
58 #-----------------------------------------------------------------------------
58 #-----------------------------------------------------------------------------
59
59
60
60
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62 # Warnings control
62 # Warnings control
63 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
64
64
65 # Twisted generates annoying warnings with Python 2.6, as will do other code
65 # Twisted generates annoying warnings with Python 2.6, as will do other code
66 # that imports 'sets' as of today
66 # that imports 'sets' as of today
67 warnings.filterwarnings('ignore', 'the sets module is deprecated',
67 warnings.filterwarnings('ignore', 'the sets module is deprecated',
68 DeprecationWarning )
68 DeprecationWarning )
69
69
70 # This one also comes from Twisted
70 # This one also comes from Twisted
71 warnings.filterwarnings('ignore', 'the sha module is deprecated',
71 warnings.filterwarnings('ignore', 'the sha module is deprecated',
72 DeprecationWarning)
72 DeprecationWarning)
73
73
74 # Wx on Fedora11 spits these out
74 # Wx on Fedora11 spits these out
75 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
75 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
76 UserWarning)
76 UserWarning)
77
77
78 # ------------------------------------------------------------------------------
78 # ------------------------------------------------------------------------------
79 # Monkeypatch Xunit to count known failures as skipped.
79 # Monkeypatch Xunit to count known failures as skipped.
80 # ------------------------------------------------------------------------------
80 # ------------------------------------------------------------------------------
81 def monkeypatch_xunit():
81 def monkeypatch_xunit():
82 try:
82 try:
83 knownfailureif(True)(lambda: None)()
83 knownfailureif(True)(lambda: None)()
84 except Exception as e:
84 except Exception as e:
85 KnownFailureTest = type(e)
85 KnownFailureTest = type(e)
86
86
87 def addError(self, test, err, capt=None):
87 def addError(self, test, err, capt=None):
88 if issubclass(err[0], KnownFailureTest):
88 if issubclass(err[0], KnownFailureTest):
89 err = (SkipTest,) + err[1:]
89 err = (SkipTest,) + err[1:]
90 return self.orig_addError(test, err, capt)
90 return self.orig_addError(test, err, capt)
91
91
92 Xunit.orig_addError = Xunit.addError
92 Xunit.orig_addError = Xunit.addError
93 Xunit.addError = addError
93 Xunit.addError = addError
94
94
95 #-----------------------------------------------------------------------------
95 #-----------------------------------------------------------------------------
96 # Check which dependencies are installed and greater than minimum version.
96 # Check which dependencies are installed and greater than minimum version.
97 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
98 def extract_version(mod):
98 def extract_version(mod):
99 return mod.__version__
99 return mod.__version__
100
100
101 def test_for(item, min_version=None, callback=extract_version):
101 def test_for(item, min_version=None, callback=extract_version):
102 """Test to see if item is importable, and optionally check against a minimum
102 """Test to see if item is importable, and optionally check against a minimum
103 version.
103 version.
104
104
105 If min_version is given, the default behavior is to check against the
105 If min_version is given, the default behavior is to check against the
106 `__version__` attribute of the item, but specifying `callback` allows you to
106 `__version__` attribute of the item, but specifying `callback` allows you to
107 extract the value you are interested in. e.g::
107 extract the value you are interested in. e.g::
108
108
109 In [1]: import sys
109 In [1]: import sys
110
110
111 In [2]: from IPython.testing.iptest import test_for
111 In [2]: from IPython.testing.iptest import test_for
112
112
113 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
113 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
114 Out[3]: True
114 Out[3]: True
115
115
116 """
116 """
117 try:
117 try:
118 check = import_item(item)
118 check = import_item(item)
119 except (ImportError, RuntimeError):
119 except (ImportError, RuntimeError):
120 # GTK reports Runtime error if it can't be initialized even if it's
120 # GTK reports Runtime error if it can't be initialized even if it's
121 # importable.
121 # importable.
122 return False
122 return False
123 else:
123 else:
124 if min_version:
124 if min_version:
125 if callback:
125 if callback:
126 # extra processing step to get version to compare
126 # extra processing step to get version to compare
127 check = callback(check)
127 check = callback(check)
128
128
129 return check >= min_version
129 return check >= min_version
130 else:
130 else:
131 return True
131 return True
132
132
133 # Global dict where we can store information on what we have and what we don't
133 # Global dict where we can store information on what we have and what we don't
134 # have available at test run time
134 # have available at test run time
135 have = {}
135 have = {}
136
136
137 have['curses'] = test_for('_curses')
137 have['curses'] = test_for('_curses')
138 have['matplotlib'] = test_for('matplotlib')
138 have['matplotlib'] = test_for('matplotlib')
139 have['numpy'] = test_for('numpy')
139 have['numpy'] = test_for('numpy')
140 have['pexpect'] = test_for('IPython.external.pexpect')
140 have['pexpect'] = test_for('IPython.external.pexpect')
141 have['pymongo'] = test_for('pymongo')
141 have['pymongo'] = test_for('pymongo')
142 have['pygments'] = test_for('pygments')
142 have['pygments'] = test_for('pygments')
143 have['qt'] = test_for('IPython.external.qt')
143 have['qt'] = test_for('IPython.external.qt')
144 have['rpy2'] = test_for('rpy2')
144 have['rpy2'] = test_for('rpy2')
145 have['sqlite3'] = test_for('sqlite3')
145 have['sqlite3'] = test_for('sqlite3')
146 have['cython'] = test_for('Cython')
146 have['cython'] = test_for('Cython')
147 have['oct2py'] = test_for('oct2py')
147 have['oct2py'] = test_for('oct2py')
148 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
148 have['tornado'] = test_for('tornado.version_info', (3,1,0), callback=None)
149 have['jinja2'] = test_for('jinja2')
149 have['jinja2'] = test_for('jinja2')
150 have['wx'] = test_for('wx')
150 have['wx'] = test_for('wx')
151 have['wx.aui'] = test_for('wx.aui')
151 have['wx.aui'] = test_for('wx.aui')
152 have['azure'] = test_for('azure')
152 have['azure'] = test_for('azure')
153 have['requests'] = test_for('requests')
153 have['requests'] = test_for('requests')
154 have['sphinx'] = test_for('sphinx')
154 have['sphinx'] = test_for('sphinx')
155 have['casperjs'] = is_cmd_found('casperjs')
155 have['casperjs'] = is_cmd_found('casperjs')
156
156
157 min_zmq = (2,1,11)
157 min_zmq = (2,1,11)
158
158
159 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
159 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
160
160
161 #-----------------------------------------------------------------------------
161 #-----------------------------------------------------------------------------
162 # Test suite definitions
162 # Test suite definitions
163 #-----------------------------------------------------------------------------
163 #-----------------------------------------------------------------------------
164
164
165 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
165 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
166 'extensions', 'lib', 'terminal', 'testing', 'utils',
166 'extensions', 'lib', 'terminal', 'testing', 'utils',
167 'nbformat', 'qt', 'html', 'js', 'nbconvert'
167 'nbformat', 'qt', 'html', 'nbconvert'
168 ]
168 ]
169
169
170 class TestSection(object):
170 class TestSection(object):
171 def __init__(self, name, includes):
171 def __init__(self, name, includes):
172 self.name = name
172 self.name = name
173 self.includes = includes
173 self.includes = includes
174 self.excludes = []
174 self.excludes = []
175 self.dependencies = []
175 self.dependencies = []
176 self.enabled = True
176 self.enabled = True
177
177
178 def exclude(self, module):
178 def exclude(self, module):
179 if not module.startswith('IPython'):
179 if not module.startswith('IPython'):
180 module = self.includes[0] + "." + module
180 module = self.includes[0] + "." + module
181 self.excludes.append(module.replace('.', os.sep))
181 self.excludes.append(module.replace('.', os.sep))
182
182
183 def requires(self, *packages):
183 def requires(self, *packages):
184 self.dependencies.extend(packages)
184 self.dependencies.extend(packages)
185
185
186 @property
186 @property
187 def will_run(self):
187 def will_run(self):
188 return self.enabled and all(have[p] for p in self.dependencies)
188 return self.enabled and all(have[p] for p in self.dependencies)
189
189
190 # Name -> (include, exclude, dependencies_met)
190 # Name -> (include, exclude, dependencies_met)
191 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
191 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
192
192
193 # Exclusions and dependencies
193 # Exclusions and dependencies
194 # ---------------------------
194 # ---------------------------
195
195
196 # core:
196 # core:
197 sec = test_sections['core']
197 sec = test_sections['core']
198 if not have['sqlite3']:
198 if not have['sqlite3']:
199 sec.exclude('tests.test_history')
199 sec.exclude('tests.test_history')
200 sec.exclude('history')
200 sec.exclude('history')
201 if not have['matplotlib']:
201 if not have['matplotlib']:
202 sec.exclude('pylabtools'),
202 sec.exclude('pylabtools'),
203 sec.exclude('tests.test_pylabtools')
203 sec.exclude('tests.test_pylabtools')
204
204
205 # lib:
205 # lib:
206 sec = test_sections['lib']
206 sec = test_sections['lib']
207 if not have['wx']:
207 if not have['wx']:
208 sec.exclude('inputhookwx')
208 sec.exclude('inputhookwx')
209 if not have['pexpect']:
209 if not have['pexpect']:
210 sec.exclude('irunner')
210 sec.exclude('irunner')
211 sec.exclude('tests.test_irunner')
211 sec.exclude('tests.test_irunner')
212 if not have['zmq']:
212 if not have['zmq']:
213 sec.exclude('kernel')
213 sec.exclude('kernel')
214 # We do this unconditionally, so that the test suite doesn't import
214 # We do this unconditionally, so that the test suite doesn't import
215 # gtk, changing the default encoding and masking some unicode bugs.
215 # gtk, changing the default encoding and masking some unicode bugs.
216 sec.exclude('inputhookgtk')
216 sec.exclude('inputhookgtk')
217 # Testing inputhook will need a lot of thought, to figure out
217 # Testing inputhook will need a lot of thought, to figure out
218 # how to have tests that don't lock up with the gui event
218 # how to have tests that don't lock up with the gui event
219 # loops in the picture
219 # loops in the picture
220 sec.exclude('inputhook')
220 sec.exclude('inputhook')
221
221
222 # testing:
222 # testing:
223 sec = test_sections['testing']
223 sec = test_sections['testing']
224 # This guy is probably attic material
224 # This guy is probably attic material
225 sec.exclude('mkdoctests')
225 sec.exclude('mkdoctests')
226 # These have to be skipped on win32 because they use echo, rm, cd, etc.
226 # These have to be skipped on win32 because they use echo, rm, cd, etc.
227 # See ticket https://github.com/ipython/ipython/issues/87
227 # See ticket https://github.com/ipython/ipython/issues/87
228 if sys.platform == 'win32':
228 if sys.platform == 'win32':
229 sec.exclude('plugin.test_exampleip')
229 sec.exclude('plugin.test_exampleip')
230 sec.exclude('plugin.dtexample')
230 sec.exclude('plugin.dtexample')
231
231
232 # terminal:
232 # terminal:
233 if (not have['pexpect']) or (not have['zmq']):
233 if (not have['pexpect']) or (not have['zmq']):
234 test_sections['terminal'].exclude('console')
234 test_sections['terminal'].exclude('console')
235
235
236 # parallel
236 # parallel
237 sec = test_sections['parallel']
237 sec = test_sections['parallel']
238 sec.requires('zmq')
238 sec.requires('zmq')
239 if not have['pymongo']:
239 if not have['pymongo']:
240 sec.exclude('controller.mongodb')
240 sec.exclude('controller.mongodb')
241 sec.exclude('tests.test_mongodb')
241 sec.exclude('tests.test_mongodb')
242
242
243 # kernel:
243 # kernel:
244 sec = test_sections['kernel']
244 sec = test_sections['kernel']
245 sec.requires('zmq')
245 sec.requires('zmq')
246 # The in-process kernel tests are done in a separate section
246 # The in-process kernel tests are done in a separate section
247 sec.exclude('inprocess')
247 sec.exclude('inprocess')
248 # importing gtk sets the default encoding, which we want to avoid
248 # importing gtk sets the default encoding, which we want to avoid
249 sec.exclude('zmq.gui.gtkembed')
249 sec.exclude('zmq.gui.gtkembed')
250 if not have['matplotlib']:
250 if not have['matplotlib']:
251 sec.exclude('zmq.pylab')
251 sec.exclude('zmq.pylab')
252
252
253 # kernel.inprocess:
253 # kernel.inprocess:
254 test_sections['kernel.inprocess'].requires('zmq')
254 test_sections['kernel.inprocess'].requires('zmq')
255
255
256 # extensions:
256 # extensions:
257 sec = test_sections['extensions']
257 sec = test_sections['extensions']
258 if not have['cython']:
258 if not have['cython']:
259 sec.exclude('cythonmagic')
259 sec.exclude('cythonmagic')
260 sec.exclude('tests.test_cythonmagic')
260 sec.exclude('tests.test_cythonmagic')
261 if not have['oct2py']:
261 if not have['oct2py']:
262 sec.exclude('octavemagic')
262 sec.exclude('octavemagic')
263 sec.exclude('tests.test_octavemagic')
263 sec.exclude('tests.test_octavemagic')
264 if not have['rpy2'] or not have['numpy']:
264 if not have['rpy2'] or not have['numpy']:
265 sec.exclude('rmagic')
265 sec.exclude('rmagic')
266 sec.exclude('tests.test_rmagic')
266 sec.exclude('tests.test_rmagic')
267 # autoreload does some strange stuff, so move it to its own test section
267 # autoreload does some strange stuff, so move it to its own test section
268 sec.exclude('autoreload')
268 sec.exclude('autoreload')
269 sec.exclude('tests.test_autoreload')
269 sec.exclude('tests.test_autoreload')
270 test_sections['autoreload'] = TestSection('autoreload',
270 test_sections['autoreload'] = TestSection('autoreload',
271 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
271 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
272 test_group_names.append('autoreload')
272 test_group_names.append('autoreload')
273
273
274 # qt:
274 # qt:
275 test_sections['qt'].requires('zmq', 'qt', 'pygments')
275 test_sections['qt'].requires('zmq', 'qt', 'pygments')
276
276
277 # html:
277 # html:
278 sec = test_sections['html']
278 sec = test_sections['html']
279 sec.requires('zmq', 'tornado', 'requests')
279 sec.requires('zmq', 'tornado', 'requests')
280 # The notebook 'static' directory contains JS, css and other
280 # The notebook 'static' directory contains JS, css and other
281 # files for web serving. Occasionally projects may put a .py
281 # files for web serving. Occasionally projects may put a .py
282 # file in there (MathJax ships a conf.py), so we might as
282 # file in there (MathJax ships a conf.py), so we might as
283 # well play it safe and skip the whole thing.
283 # well play it safe and skip the whole thing.
284 sec.exclude('static')
284 sec.exclude('static')
285 sec.exclude('fabfile')
285 sec.exclude('fabfile')
286 if not have['jinja2']:
286 if not have['jinja2']:
287 sec.exclude('notebookapp')
287 sec.exclude('notebookapp')
288 if not have['azure']:
288 if not have['azure']:
289 sec.exclude('services.notebooks.azurenbmanager')
289 sec.exclude('services.notebooks.azurenbmanager')
290
290
291 sec = test_sections['js']
292 sec.requires('zmq', 'tornado', 'jinja2', 'casperjs')
293
294 # config:
291 # config:
295 # Config files aren't really importable stand-alone
292 # Config files aren't really importable stand-alone
296 test_sections['config'].exclude('profile')
293 test_sections['config'].exclude('profile')
297
294
298 # nbconvert:
295 # nbconvert:
299 sec = test_sections['nbconvert']
296 sec = test_sections['nbconvert']
300 sec.requires('pygments', 'jinja2', 'sphinx')
297 sec.requires('pygments', 'jinja2', 'sphinx')
301 # Exclude nbconvert directories containing config files used to test.
298 # Exclude nbconvert directories containing config files used to test.
302 # Executing the config files with iptest would cause an exception.
299 # Executing the config files with iptest would cause an exception.
303 sec.exclude('tests.files')
300 sec.exclude('tests.files')
304 sec.exclude('exporters.tests.files')
301 sec.exclude('exporters.tests.files')
305 if not have['tornado']:
302 if not have['tornado']:
306 sec.exclude('nbconvert.post_processors.serve')
303 sec.exclude('nbconvert.post_processors.serve')
307 sec.exclude('nbconvert.post_processors.tests.test_serve')
304 sec.exclude('nbconvert.post_processors.tests.test_serve')
308
305
309 #-----------------------------------------------------------------------------
306 #-----------------------------------------------------------------------------
310 # Functions and classes
307 # Functions and classes
311 #-----------------------------------------------------------------------------
308 #-----------------------------------------------------------------------------
312
309
313 def check_exclusions_exist():
310 def check_exclusions_exist():
314 from IPython.utils.path import get_ipython_package_dir
311 from IPython.utils.path import get_ipython_package_dir
315 from IPython.utils.warn import warn
312 from IPython.utils.warn import warn
316 parent = os.path.dirname(get_ipython_package_dir())
313 parent = os.path.dirname(get_ipython_package_dir())
317 for sec in test_sections:
314 for sec in test_sections:
318 for pattern in sec.exclusions:
315 for pattern in sec.exclusions:
319 fullpath = pjoin(parent, pattern)
316 fullpath = pjoin(parent, pattern)
320 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
317 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
321 warn("Excluding nonexistent file: %r" % pattern)
318 warn("Excluding nonexistent file: %r" % pattern)
322
319
323
320
324 class ExclusionPlugin(Plugin):
321 class ExclusionPlugin(Plugin):
325 """A nose plugin to effect our exclusions of files and directories.
322 """A nose plugin to effect our exclusions of files and directories.
326 """
323 """
327 name = 'exclusions'
324 name = 'exclusions'
328 score = 3000 # Should come before any other plugins
325 score = 3000 # Should come before any other plugins
329
326
330 def __init__(self, exclude_patterns=None):
327 def __init__(self, exclude_patterns=None):
331 """
328 """
332 Parameters
329 Parameters
333 ----------
330 ----------
334
331
335 exclude_patterns : sequence of strings, optional
332 exclude_patterns : sequence of strings, optional
336 Filenames containing these patterns (as raw strings, not as regular
333 Filenames containing these patterns (as raw strings, not as regular
337 expressions) are excluded from the tests.
334 expressions) are excluded from the tests.
338 """
335 """
339 self.exclude_patterns = exclude_patterns or []
336 self.exclude_patterns = exclude_patterns or []
340 super(ExclusionPlugin, self).__init__()
337 super(ExclusionPlugin, self).__init__()
341
338
342 def options(self, parser, env=os.environ):
339 def options(self, parser, env=os.environ):
343 Plugin.options(self, parser, env)
340 Plugin.options(self, parser, env)
344
341
345 def configure(self, options, config):
342 def configure(self, options, config):
346 Plugin.configure(self, options, config)
343 Plugin.configure(self, options, config)
347 # Override nose trying to disable plugin.
344 # Override nose trying to disable plugin.
348 self.enabled = True
345 self.enabled = True
349
346
350 def wantFile(self, filename):
347 def wantFile(self, filename):
351 """Return whether the given filename should be scanned for tests.
348 """Return whether the given filename should be scanned for tests.
352 """
349 """
353 if any(pat in filename for pat in self.exclude_patterns):
350 if any(pat in filename for pat in self.exclude_patterns):
354 return False
351 return False
355 return None
352 return None
356
353
357 def wantDirectory(self, directory):
354 def wantDirectory(self, directory):
358 """Return whether the given directory should be scanned for tests.
355 """Return whether the given directory should be scanned for tests.
359 """
356 """
360 if any(pat in directory for pat in self.exclude_patterns):
357 if any(pat in directory for pat in self.exclude_patterns):
361 return False
358 return False
362 return None
359 return None
363
360
364
361
365 class StreamCapturer(Thread):
362 class StreamCapturer(Thread):
366 started = False
363 started = False
367 def __init__(self):
364 def __init__(self):
368 super(StreamCapturer, self).__init__()
365 super(StreamCapturer, self).__init__()
369 self.streams = []
366 self.streams = []
370 self.buffer = BytesIO()
367 self.buffer = BytesIO()
371 self.streams_lock = Lock()
368 self.streams_lock = Lock()
372 self.buffer_lock = Lock()
369 self.buffer_lock = Lock()
373 self.stream_added = Event()
370 self.stream_added = Event()
374 self.stop = Event()
371 self.stop = Event()
375
372
376 def run(self):
373 def run(self):
377 self.started = True
374 self.started = True
378 while not self.stop.is_set():
375 while not self.stop.is_set():
379 with self.streams_lock:
376 with self.streams_lock:
380 streams = self.streams
377 streams = self.streams
381
378
382 if not streams:
379 if not streams:
383 self.stream_added.wait(timeout=1)
380 self.stream_added.wait(timeout=1)
384 self.stream_added.clear()
381 self.stream_added.clear()
385 continue
382 continue
386
383
387 ready = select(streams, [], [], 0.5)[0]
384 ready = select(streams, [], [], 0.5)[0]
388 with self.buffer_lock:
385 with self.buffer_lock:
389 for fd in ready:
386 for fd in ready:
390 self.buffer.write(os.read(fd, 1024))
387 self.buffer.write(os.read(fd, 1024))
391
388
392 def add_stream(self, fd):
389 def add_stream(self, fd):
393 with self.streams_lock:
390 with self.streams_lock:
394 self.streams.append(fd)
391 self.streams.append(fd)
395 self.stream_added.set()
392 self.stream_added.set()
396
393
397 def remove_stream(self, fd):
394 def remove_stream(self, fd):
398 with self.streams_lock:
395 with self.streams_lock:
399 self.streams.remove(fd)
396 self.streams.remove(fd)
400
397
401 def reset_buffer(self):
398 def reset_buffer(self):
402 with self.buffer_lock:
399 with self.buffer_lock:
403 self.buffer.truncate(0)
400 self.buffer.truncate(0)
404 self.buffer.seek(0)
401 self.buffer.seek(0)
405
402
406 def get_buffer(self):
403 def get_buffer(self):
407 with self.buffer_lock:
404 with self.buffer_lock:
408 return self.buffer.getvalue()
405 return self.buffer.getvalue()
409
406
410 def ensure_started(self):
407 def ensure_started(self):
411 if not self.started:
408 if not self.started:
412 self.start()
409 self.start()
413
410
414 class SubprocessStreamCapturePlugin(Plugin):
411 class SubprocessStreamCapturePlugin(Plugin):
415 name='subprocstreams'
412 name='subprocstreams'
416 def __init__(self):
413 def __init__(self):
417 Plugin.__init__(self)
414 Plugin.__init__(self)
418 self.stream_capturer = StreamCapturer()
415 self.stream_capturer = StreamCapturer()
419 # This is ugly, but distant parts of the test machinery need to be able
416 # This is ugly, but distant parts of the test machinery need to be able
420 # to add streams, so we make the object globally accessible.
417 # to add streams, so we make the object globally accessible.
421 nose.ipy_stream_capturer = self.stream_capturer
418 nose.ipy_stream_capturer = self.stream_capturer
422
419
423 def configure(self, options, config):
420 def configure(self, options, config):
424 Plugin.configure(self, options, config)
421 Plugin.configure(self, options, config)
425 # Override nose trying to disable plugin.
422 # Override nose trying to disable plugin.
426 self.enabled = True
423 self.enabled = True
427
424
428 def startTest(self, test):
425 def startTest(self, test):
429 # Reset log capture
426 # Reset log capture
430 self.stream_capturer.reset_buffer()
427 self.stream_capturer.reset_buffer()
431
428
432 def formatFailure(self, test, err):
429 def formatFailure(self, test, err):
433 # Show output
430 # Show output
434 ec, ev, tb = err
431 ec, ev, tb = err
435 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
432 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
436 if captured.strip():
433 if captured.strip():
437 ev = safe_str(ev)
434 ev = safe_str(ev)
438 out = [ev, '>> begin captured subprocess output <<',
435 out = [ev, '>> begin captured subprocess output <<',
439 captured,
436 captured,
440 '>> end captured subprocess output <<']
437 '>> end captured subprocess output <<']
441 return ec, '\n'.join(out), tb
438 return ec, '\n'.join(out), tb
442
439
443 return err
440 return err
444
441
445 formatError = formatFailure
442 formatError = formatFailure
446
443
447 def finalize(self, result):
444 def finalize(self, result):
448 if self.stream_capturer.started:
445 if self.stream_capturer.started:
449 self.stream_capturer.stop.set()
446 self.stream_capturer.stop.set()
450 self.stream_capturer.join()
447 self.stream_capturer.join()
451
448
452
449
453 def run_iptest():
450 def run_iptest():
454 """Run the IPython test suite using nose.
451 """Run the IPython test suite using nose.
455
452
456 This function is called when this script is **not** called with the form
453 This function is called when this script is **not** called with the form
457 `iptest all`. It simply calls nose with appropriate command line flags
454 `iptest all`. It simply calls nose with appropriate command line flags
458 and accepts all of the standard nose arguments.
455 and accepts all of the standard nose arguments.
459 """
456 """
460 # Apply our monkeypatch to Xunit
457 # Apply our monkeypatch to Xunit
461 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
458 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
462 monkeypatch_xunit()
459 monkeypatch_xunit()
463
460
464 warnings.filterwarnings('ignore',
461 warnings.filterwarnings('ignore',
465 'This will be removed soon. Use IPython.testing.util instead')
462 'This will be removed soon. Use IPython.testing.util instead')
466
463
467 arg1 = sys.argv[1]
464 arg1 = sys.argv[1]
468 if arg1 in test_sections:
465 if arg1 in test_sections:
469 section = test_sections[arg1]
466 section = test_sections[arg1]
470 sys.argv[1:2] = section.includes
467 sys.argv[1:2] = section.includes
471 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
468 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
472 section = test_sections[arg1[8:]]
469 section = test_sections[arg1[8:]]
473 sys.argv[1:2] = section.includes
470 sys.argv[1:2] = section.includes
474 else:
471 else:
475 section = TestSection(arg1, includes=[arg1])
472 section = TestSection(arg1, includes=[arg1])
476
473
477
474
478 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
475 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
479
476
480 '--with-ipdoctest',
477 '--with-ipdoctest',
481 '--ipdoctest-tests','--ipdoctest-extension=txt',
478 '--ipdoctest-tests','--ipdoctest-extension=txt',
482
479
483 # We add --exe because of setuptools' imbecility (it
480 # We add --exe because of setuptools' imbecility (it
484 # blindly does chmod +x on ALL files). Nose does the
481 # blindly does chmod +x on ALL files). Nose does the
485 # right thing and it tries to avoid executables,
482 # right thing and it tries to avoid executables,
486 # setuptools unfortunately forces our hand here. This
483 # setuptools unfortunately forces our hand here. This
487 # has been discussed on the distutils list and the
484 # has been discussed on the distutils list and the
488 # setuptools devs refuse to fix this problem!
485 # setuptools devs refuse to fix this problem!
489 '--exe',
486 '--exe',
490 ]
487 ]
491 if '-a' not in argv and '-A' not in argv:
488 if '-a' not in argv and '-A' not in argv:
492 argv = argv + ['-a', '!crash']
489 argv = argv + ['-a', '!crash']
493
490
494 if nose.__version__ >= '0.11':
491 if nose.__version__ >= '0.11':
495 # I don't fully understand why we need this one, but depending on what
492 # I don't fully understand why we need this one, but depending on what
496 # directory the test suite is run from, if we don't give it, 0 tests
493 # directory the test suite is run from, if we don't give it, 0 tests
497 # get run. Specifically, if the test suite is run from the source dir
494 # get run. Specifically, if the test suite is run from the source dir
498 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
495 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
499 # even if the same call done in this directory works fine). It appears
496 # even if the same call done in this directory works fine). It appears
500 # that if the requested package is in the current dir, nose bails early
497 # that if the requested package is in the current dir, nose bails early
501 # by default. Since it's otherwise harmless, leave it in by default
498 # by default. Since it's otherwise harmless, leave it in by default
502 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
499 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
503 argv.append('--traverse-namespace')
500 argv.append('--traverse-namespace')
504
501
505 # use our plugin for doctesting. It will remove the standard doctest plugin
502 # use our plugin for doctesting. It will remove the standard doctest plugin
506 # if it finds it enabled
503 # if it finds it enabled
507 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
504 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
508 SubprocessStreamCapturePlugin() ]
505 SubprocessStreamCapturePlugin() ]
509
506
510 # Use working directory set by parent process (see iptestcontroller)
507 # Use working directory set by parent process (see iptestcontroller)
511 if 'IPTEST_WORKING_DIR' in os.environ:
508 if 'IPTEST_WORKING_DIR' in os.environ:
512 os.chdir(os.environ['IPTEST_WORKING_DIR'])
509 os.chdir(os.environ['IPTEST_WORKING_DIR'])
513
510
514 # We need a global ipython running in this process, but the special
511 # We need a global ipython running in this process, but the special
515 # in-process group spawns its own IPython kernels, so for *that* group we
512 # in-process group spawns its own IPython kernels, so for *that* group we
516 # must avoid also opening the global one (otherwise there's a conflict of
513 # must avoid also opening the global one (otherwise there's a conflict of
517 # singletons). Ultimately the solution to this problem is to refactor our
514 # singletons). Ultimately the solution to this problem is to refactor our
518 # assumptions about what needs to be a singleton and what doesn't (app
515 # assumptions about what needs to be a singleton and what doesn't (app
519 # objects should, individual shells shouldn't). But for now, this
516 # objects should, individual shells shouldn't). But for now, this
520 # workaround allows the test suite for the inprocess module to complete.
517 # workaround allows the test suite for the inprocess module to complete.
521 if 'kernel.inprocess' not in section.name:
518 if 'kernel.inprocess' not in section.name:
522 from IPython.testing import globalipapp
519 from IPython.testing import globalipapp
523 globalipapp.start_ipython()
520 globalipapp.start_ipython()
524
521
525 # Now nose can run
522 # Now nose can run
526 TestProgram(argv=argv, addplugins=plugins)
523 TestProgram(argv=argv, addplugins=plugins)
527
524
528 if __name__ == '__main__':
525 if __name__ == '__main__':
529 run_iptest()
526 run_iptest()
530
527
@@ -1,467 +1,477 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 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2009-2011 The IPython Development Team
10 # Copyright (C) 2009-2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
19 from __future__ import print_function
20
20
21 import argparse
21 import argparse
22 import multiprocessing.pool
22 import multiprocessing.pool
23 from multiprocessing import Process, Queue
23 from multiprocessing import Process, Queue
24 import os
24 import os
25 import shutil
25 import shutil
26 import signal
26 import signal
27 import sys
27 import sys
28 import subprocess
28 import subprocess
29 import time
29 import time
30
30
31 from .iptest import have, test_group_names, test_sections
31 from .iptest import have, test_group_names as py_test_group_names, test_sections
32 from IPython.utils.py3compat import bytes_to_str
32 from IPython.utils.py3compat import bytes_to_str
33 from IPython.utils.sysinfo import sys_info
33 from IPython.utils.sysinfo import sys_info
34 from IPython.utils.tempdir import TemporaryDirectory
34 from IPython.utils.tempdir import TemporaryDirectory
35
35
36
36
37 class TestController(object):
37 class TestController(object):
38 """Run tests in a subprocess
38 """Run tests in a subprocess
39 """
39 """
40 #: str, IPython test suite to be executed.
40 #: str, IPython test suite to be executed.
41 section = None
41 section = None
42 #: list, command line arguments to be executed
42 #: list, command line arguments to be executed
43 cmd = None
43 cmd = None
44 #: dict, extra environment variables to set for the subprocess
44 #: dict, extra environment variables to set for the subprocess
45 env = None
45 env = None
46 #: list, TemporaryDirectory instances to clear up when the process finishes
46 #: list, TemporaryDirectory instances to clear up when the process finishes
47 dirs = None
47 dirs = None
48 #: subprocess.Popen instance
48 #: subprocess.Popen instance
49 process = None
49 process = None
50 #: str, process stdout+stderr
50 #: str, process stdout+stderr
51 stdout = None
51 stdout = None
52 #: bool, whether to capture process stdout & stderr
52 #: bool, whether to capture process stdout & stderr
53 buffer_output = False
53 buffer_output = False
54
54
55 def __init__(self):
55 def __init__(self):
56 self.cmd = []
56 self.cmd = []
57 self.env = {}
57 self.env = {}
58 self.dirs = []
58 self.dirs = []
59
60
61 @property
62 def will_run(self):
63 try:
64 return test_sections[self.section].will_run
65 except KeyError:
66 return True
67
59
68 def launch(self):
60 def launch(self):
69 # print('*** ENV:', self.env) # dbg
61 # print('*** ENV:', self.env) # dbg
70 # print('*** CMD:', self.cmd) # dbg
62 # print('*** CMD:', self.cmd) # dbg
71 env = os.environ.copy()
63 env = os.environ.copy()
72 env.update(self.env)
64 env.update(self.env)
73 output = subprocess.PIPE if self.buffer_output else None
65 output = subprocess.PIPE if self.buffer_output else None
74 stdout = subprocess.STDOUT if self.buffer_output else None
66 stdout = subprocess.STDOUT if self.buffer_output else None
75 self.process = subprocess.Popen(self.cmd, stdout=output,
67 self.process = subprocess.Popen(self.cmd, stdout=output,
76 stderr=stdout, env=env)
68 stderr=stdout, env=env)
77
69
78 def wait(self):
70 def wait(self):
79 self.stdout, _ = self.process.communicate()
71 self.stdout, _ = self.process.communicate()
80 return self.process.returncode
72 return self.process.returncode
81
73
82 def cleanup_process(self):
74 def cleanup_process(self):
83 """Cleanup on exit by killing any leftover processes."""
75 """Cleanup on exit by killing any leftover processes."""
84 subp = self.process
76 subp = self.process
85 if subp is None or (subp.poll() is not None):
77 if subp is None or (subp.poll() is not None):
86 return # Process doesn't exist, or is already dead.
78 return # Process doesn't exist, or is already dead.
87
79
88 try:
80 try:
89 print('Cleaning up stale PID: %d' % subp.pid)
81 print('Cleaning up stale PID: %d' % subp.pid)
90 subp.kill()
82 subp.kill()
91 except: # (OSError, WindowsError) ?
83 except: # (OSError, WindowsError) ?
92 # This is just a best effort, if we fail or the process was
84 # This is just a best effort, if we fail or the process was
93 # really gone, ignore it.
85 # really gone, ignore it.
94 pass
86 pass
95 else:
87 else:
96 for i in range(10):
88 for i in range(10):
97 if subp.poll() is None:
89 if subp.poll() is None:
98 time.sleep(0.1)
90 time.sleep(0.1)
99 else:
91 else:
100 break
92 break
101
93
102 if subp.poll() is None:
94 if subp.poll() is None:
103 # The process did not die...
95 # The process did not die...
104 print('... failed. Manual cleanup may be required.')
96 print('... failed. Manual cleanup may be required.')
105
97
106 def cleanup(self):
98 def cleanup(self):
107 "Kill process if it's still alive, and clean up temporary directories"
99 "Kill process if it's still alive, and clean up temporary directories"
108 self.cleanup_process()
100 self.cleanup_process()
109 for td in self.dirs:
101 for td in self.dirs:
110 td.cleanup()
102 td.cleanup()
111
103
112 __del__ = cleanup
104 __del__ = cleanup
113
105
114 class PyTestController(TestController):
106 class PyTestController(TestController):
115 """Run Python tests using IPython.testing.iptest"""
107 """Run Python tests using IPython.testing.iptest"""
116 #: str, Python command to execute in subprocess
108 #: str, Python command to execute in subprocess
117 pycmd = None
109 pycmd = None
118
110
119 def __init__(self, section):
111 def __init__(self, section):
120 """Create new test runner."""
112 """Create new test runner."""
121 TestController.__init__(self)
113 TestController.__init__(self)
122 self.section = section
114 self.section = section
123 # pycmd is put into cmd[2] in PyTestController.launch()
115 # pycmd is put into cmd[2] in PyTestController.launch()
124 self.cmd = [sys.executable, '-c', None, section]
116 self.cmd = [sys.executable, '-c', None, section]
125 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
117 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
126 ipydir = TemporaryDirectory()
118 ipydir = TemporaryDirectory()
127 self.dirs.append(ipydir)
119 self.dirs.append(ipydir)
128 self.env['IPYTHONDIR'] = ipydir.name
120 self.env['IPYTHONDIR'] = ipydir.name
129 self.workingdir = workingdir = TemporaryDirectory()
121 self.workingdir = workingdir = TemporaryDirectory()
130 self.dirs.append(workingdir)
122 self.dirs.append(workingdir)
131 self.env['IPTEST_WORKING_DIR'] = workingdir.name
123 self.env['IPTEST_WORKING_DIR'] = workingdir.name
132 # This means we won't get odd effects from our own matplotlib config
124 # This means we won't get odd effects from our own matplotlib config
133 self.env['MPLCONFIGDIR'] = workingdir.name
125 self.env['MPLCONFIGDIR'] = workingdir.name
134
126
127 @property
128 def will_run(self):
129 try:
130 return test_sections[self.section].will_run
131 except KeyError:
132 return True
133
135 def add_xunit(self):
134 def add_xunit(self):
136 xunit_file = os.path.abspath(self.section + '.xunit.xml')
135 xunit_file = os.path.abspath(self.section + '.xunit.xml')
137 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
136 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
138
137
139 def add_coverage(self):
138 def add_coverage(self):
140 try:
139 try:
141 sources = test_sections[self.section].includes
140 sources = test_sections[self.section].includes
142 except KeyError:
141 except KeyError:
143 sources = ['IPython']
142 sources = ['IPython']
144
143
145 coverage_rc = ("[run]\n"
144 coverage_rc = ("[run]\n"
146 "data_file = {data_file}\n"
145 "data_file = {data_file}\n"
147 "source =\n"
146 "source =\n"
148 " {source}\n"
147 " {source}\n"
149 ).format(data_file=os.path.abspath('.coverage.'+self.section),
148 ).format(data_file=os.path.abspath('.coverage.'+self.section),
150 source="\n ".join(sources))
149 source="\n ".join(sources))
151 config_file = os.path.join(self.workingdir.name, '.coveragerc')
150 config_file = os.path.join(self.workingdir.name, '.coveragerc')
152 with open(config_file, 'w') as f:
151 with open(config_file, 'w') as f:
153 f.write(coverage_rc)
152 f.write(coverage_rc)
154
153
155 self.env['COVERAGE_PROCESS_START'] = config_file
154 self.env['COVERAGE_PROCESS_START'] = config_file
156 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
155 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
157
156
158 def launch(self):
157 def launch(self):
159 self.cmd[2] = self.pycmd
158 self.cmd[2] = self.pycmd
160 super(PyTestController, self).launch()
159 super(PyTestController, self).launch()
161
160
162 class JSController(TestController):
161 class JSController(TestController):
163 """Run CasperJS tests """
162 """Run CasperJS tests """
164 def __init__(self, section):
163 def __init__(self, section):
165 """Create new test runner."""
164 """Create new test runner."""
166 TestController.__init__(self)
165 TestController.__init__(self)
167 self.section = section
166 self.section = section
168
167
169 self.ipydir = TemporaryDirectory()
168 self.ipydir = TemporaryDirectory()
170 self.dirs.append(self.ipydir)
169 self.dirs.append(self.ipydir)
171 self.env['IPYTHONDIR'] = self.ipydir.name
170 self.env['IPYTHONDIR'] = self.ipydir.name
172
171
172 def launch(self):
173 # start the ipython notebook, so we get the port number
173 # start the ipython notebook, so we get the port number
174 self._init_server()
174 self._init_server()
175
175
176 import IPython.html.tests as t
176 import IPython.html.tests as t
177 test_dir = os.path.join(os.path.dirname(t.__file__), 'casperjs')
177 test_dir = os.path.join(os.path.dirname(t.__file__), 'casperjs')
178 includes = '--includes=' + os.path.join(test_dir,'util.js')
178 includes = '--includes=' + os.path.join(test_dir,'util.js')
179 test_cases = os.path.join(test_dir, 'test_cases')
179 test_cases = os.path.join(test_dir, 'test_cases')
180 port = '--port=' + str(self.server_port)
180 port = '--port=' + str(self.server_port)
181 self.cmd = ['casperjs', 'test', port, includes, test_cases]
181 self.cmd = ['casperjs', 'test', port, includes, test_cases]
182
182
183 super(JSController, self).launch()
184
185 @property
186 def will_run(self):
187 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
183
188
184 def _init_server(self):
189 def _init_server(self):
185 "Start the notebook server in a separate process"
190 "Start the notebook server in a separate process"
186 self.queue = q = Queue()
191 self.queue = q = Queue()
187 self.server = Process(target=run_webapp, args=(q, self.ipydir.name))
192 self.server = Process(target=run_webapp, args=(q, self.ipydir.name))
188 self.server.start()
193 self.server.start()
189 self.server_port = q.get()
194 self.server_port = q.get()
190
195
191 def cleanup(self):
196 def cleanup(self):
192 self.server.terminate()
197 self.server.terminate()
193 self.server.join()
198 self.server.join()
194 TestController.cleanup(self)
199 TestController.cleanup(self)
195
200
201 js_test_group_names = {'js'}
196
202
197 def run_webapp(q, nbdir, loglevel=0):
203 def run_webapp(q, nbdir, loglevel=0):
198 """start the IPython Notebook, and pass port back to the queue"""
204 """start the IPython Notebook, and pass port back to the queue"""
199 import IPython.html.notebookapp as nbapp
205 import IPython.html.notebookapp as nbapp
200 server = nbapp.NotebookApp()
206 server = nbapp.NotebookApp()
201 args = ['--no-browser']
207 args = ['--no-browser']
202 args.append('--notebook-dir='+nbdir)
208 args.append('--notebook-dir='+nbdir)
203 args.append('--profile-dir='+nbdir)
209 args.append('--profile-dir='+nbdir)
204 args.append('--log-level='+str(loglevel))
210 args.append('--log-level='+str(loglevel))
205 server.initialize(args)
211 server.initialize(args)
206 # communicate the port number to the parent process
212 # communicate the port number to the parent process
207 q.put(server.port)
213 q.put(server.port)
208 server.start()
214 server.start()
209
215
210 def prepare_controllers(options):
216 def prepare_controllers(options):
211 """Returns two lists of TestController instances, those to run, and those
217 """Returns two lists of TestController instances, those to run, and those
212 not to run."""
218 not to run."""
213 testgroups = options.testgroups
219 testgroups = options.testgroups
214
220
215 if not testgroups:
221 if testgroups:
216 testgroups = test_group_names
222 py_testgroups = [g for g in testgroups if g in py_test_group_names]
223 js_testgroups = [g for g in testgroups if g in js_test_group_names]
224 else:
225 py_testgroups = py_test_group_names
226 js_testgroups = js_test_group_names
217 if not options.all:
227 if not options.all:
218 test_sections['parallel'].enabled = False
228 test_sections['parallel'].enabled = False
219
229
220 c_js = [JSController(name) for name in testgroups if 'js' in name]
230 c_js = [JSController(name) for name in js_testgroups]
221 c_py = [PyTestController(name) for name in testgroups if 'js' not in name]
231 c_py = [PyTestController(name) for name in py_testgroups]
222
232
223 configure_py_controllers(c_py, xunit=options.xunit,
233 configure_py_controllers(c_py, xunit=options.xunit,
224 coverage=options.coverage)
234 coverage=options.coverage)
225
235
226 controllers = c_py + c_js
236 controllers = c_py + c_js
227 to_run = [c for c in controllers if c.will_run]
237 to_run = [c for c in controllers if c.will_run]
228 not_run = [c for c in controllers if not c.will_run]
238 not_run = [c for c in controllers if not c.will_run]
229 return to_run, not_run
239 return to_run, not_run
230
240
231 def configure_py_controllers(controllers, xunit=False, coverage=False, extra_args=()):
241 def configure_py_controllers(controllers, xunit=False, coverage=False, extra_args=()):
232 """Apply options for a collection of TestController objects."""
242 """Apply options for a collection of TestController objects."""
233 for controller in controllers:
243 for controller in controllers:
234 if xunit:
244 if xunit:
235 controller.add_xunit()
245 controller.add_xunit()
236 if coverage:
246 if coverage:
237 controller.add_coverage()
247 controller.add_coverage()
238 controller.cmd.extend(extra_args)
248 controller.cmd.extend(extra_args)
239
249
240 def do_run(controller):
250 def do_run(controller):
241 try:
251 try:
242 try:
252 try:
243 controller.launch()
253 controller.launch()
244 except Exception:
254 except Exception:
245 import traceback
255 import traceback
246 traceback.print_exc()
256 traceback.print_exc()
247 return controller, 1 # signal failure
257 return controller, 1 # signal failure
248
258
249 exitcode = controller.wait()
259 exitcode = controller.wait()
250 return controller, exitcode
260 return controller, exitcode
251
261
252 except KeyboardInterrupt:
262 except KeyboardInterrupt:
253 return controller, -signal.SIGINT
263 return controller, -signal.SIGINT
254 finally:
264 finally:
255 controller.cleanup()
265 controller.cleanup()
256
266
257 def report():
267 def report():
258 """Return a string with a summary report of test-related variables."""
268 """Return a string with a summary report of test-related variables."""
259
269
260 out = [ sys_info(), '\n']
270 out = [ sys_info(), '\n']
261
271
262 avail = []
272 avail = []
263 not_avail = []
273 not_avail = []
264
274
265 for k, is_avail in have.items():
275 for k, is_avail in have.items():
266 if is_avail:
276 if is_avail:
267 avail.append(k)
277 avail.append(k)
268 else:
278 else:
269 not_avail.append(k)
279 not_avail.append(k)
270
280
271 if avail:
281 if avail:
272 out.append('\nTools and libraries available at test time:\n')
282 out.append('\nTools and libraries available at test time:\n')
273 avail.sort()
283 avail.sort()
274 out.append(' ' + ' '.join(avail)+'\n')
284 out.append(' ' + ' '.join(avail)+'\n')
275
285
276 if not_avail:
286 if not_avail:
277 out.append('\nTools and libraries NOT available at test time:\n')
287 out.append('\nTools and libraries NOT available at test time:\n')
278 not_avail.sort()
288 not_avail.sort()
279 out.append(' ' + ' '.join(not_avail)+'\n')
289 out.append(' ' + ' '.join(not_avail)+'\n')
280
290
281 return ''.join(out)
291 return ''.join(out)
282
292
283 def run_iptestall(options):
293 def run_iptestall(options):
284 """Run the entire IPython test suite by calling nose and trial.
294 """Run the entire IPython test suite by calling nose and trial.
285
295
286 This function constructs :class:`IPTester` instances for all IPython
296 This function constructs :class:`IPTester` instances for all IPython
287 modules and package and then runs each of them. This causes the modules
297 modules and package and then runs each of them. This causes the modules
288 and packages of IPython to be tested each in their own subprocess using
298 and packages of IPython to be tested each in their own subprocess using
289 nose.
299 nose.
290
300
291 Parameters
301 Parameters
292 ----------
302 ----------
293
303
294 All parameters are passed as attributes of the options object.
304 All parameters are passed as attributes of the options object.
295
305
296 testgroups : list of str
306 testgroups : list of str
297 Run only these sections of the test suite. If empty, run all the available
307 Run only these sections of the test suite. If empty, run all the available
298 sections.
308 sections.
299
309
300 fast : int or None
310 fast : int or None
301 Run the test suite in parallel, using n simultaneous processes. If None
311 Run the test suite in parallel, using n simultaneous processes. If None
302 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
312 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
303
313
304 inc_slow : bool
314 inc_slow : bool
305 Include slow tests, like IPython.parallel. By default, these tests aren't
315 Include slow tests, like IPython.parallel. By default, these tests aren't
306 run.
316 run.
307
317
308 xunit : bool
318 xunit : bool
309 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
319 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
310
320
311 coverage : bool or str
321 coverage : bool or str
312 Measure code coverage from tests. True will store the raw coverage data,
322 Measure code coverage from tests. True will store the raw coverage data,
313 or pass 'html' or 'xml' to get reports.
323 or pass 'html' or 'xml' to get reports.
314
324
315 extra_args : list
325 extra_args : list
316 Extra arguments to pass to the test subprocesses, e.g. '-v'
326 Extra arguments to pass to the test subprocesses, e.g. '-v'
317 """
327 """
318 if options.fast != 1:
328 if options.fast != 1:
319 # If running in parallel, capture output so it doesn't get interleaved
329 # If running in parallel, capture output so it doesn't get interleaved
320 TestController.buffer_output = True
330 TestController.buffer_output = True
321
331
322 to_run, not_run = prepare_controllers(options)
332 to_run, not_run = prepare_controllers(options)
323
333
324 def justify(ltext, rtext, width=70, fill='-'):
334 def justify(ltext, rtext, width=70, fill='-'):
325 ltext += ' '
335 ltext += ' '
326 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
336 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
327 return ltext + rtext
337 return ltext + rtext
328
338
329 # Run all test runners, tracking execution time
339 # Run all test runners, tracking execution time
330 failed = []
340 failed = []
331 t_start = time.time()
341 t_start = time.time()
332
342
333 print()
343 print()
334 if options.fast == 1:
344 if options.fast == 1:
335 # This actually means sequential, i.e. with 1 job
345 # This actually means sequential, i.e. with 1 job
336 for controller in to_run:
346 for controller in to_run:
337 print('IPython test group:', controller.section)
347 print('IPython test group:', controller.section)
338 sys.stdout.flush() # Show in correct order when output is piped
348 sys.stdout.flush() # Show in correct order when output is piped
339 controller, res = do_run(controller)
349 controller, res = do_run(controller)
340 if res:
350 if res:
341 failed.append(controller)
351 failed.append(controller)
342 if res == -signal.SIGINT:
352 if res == -signal.SIGINT:
343 print("Interrupted")
353 print("Interrupted")
344 break
354 break
345 print()
355 print()
346
356
347 else:
357 else:
348 # Run tests concurrently
358 # Run tests concurrently
349 try:
359 try:
350 pool = multiprocessing.pool.ThreadPool(options.fast)
360 pool = multiprocessing.pool.ThreadPool(options.fast)
351 for (controller, res) in pool.imap_unordered(do_run, to_run):
361 for (controller, res) in pool.imap_unordered(do_run, to_run):
352 res_string = 'OK' if res == 0 else 'FAILED'
362 res_string = 'OK' if res == 0 else 'FAILED'
353 print(justify('IPython test group: ' + controller.section, res_string))
363 print(justify('IPython test group: ' + controller.section, res_string))
354 if res:
364 if res:
355 print(bytes_to_str(controller.stdout))
365 print(bytes_to_str(controller.stdout))
356 failed.append(controller)
366 failed.append(controller)
357 if res == -signal.SIGINT:
367 if res == -signal.SIGINT:
358 print("Interrupted")
368 print("Interrupted")
359 break
369 break
360 except KeyboardInterrupt:
370 except KeyboardInterrupt:
361 return
371 return
362
372
363 for controller in not_run:
373 for controller in not_run:
364 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
374 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
365
375
366 t_end = time.time()
376 t_end = time.time()
367 t_tests = t_end - t_start
377 t_tests = t_end - t_start
368 nrunners = len(to_run)
378 nrunners = len(to_run)
369 nfail = len(failed)
379 nfail = len(failed)
370 # summarize results
380 # summarize results
371 print('_'*70)
381 print('_'*70)
372 print('Test suite completed for system with the following information:')
382 print('Test suite completed for system with the following information:')
373 print(report())
383 print(report())
374 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
384 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
375 print()
385 print()
376 print('Status: ', end='')
386 print('Status: ', end='')
377 if not failed:
387 if not failed:
378 print('OK')
388 print('OK')
379 else:
389 else:
380 # If anything went wrong, point out what command to rerun manually to
390 # If anything went wrong, point out what command to rerun manually to
381 # see the actual errors and individual summary
391 # see the actual errors and individual summary
382 failed_sections = [c.section for c in failed]
392 failed_sections = [c.section for c in failed]
383 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
393 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
384 nrunners, ', '.join(failed_sections)))
394 nrunners, ', '.join(failed_sections)))
385 print()
395 print()
386 print('You may wish to rerun these, with:')
396 print('You may wish to rerun these, with:')
387 print(' iptest', *failed_sections)
397 print(' iptest', *failed_sections)
388 print()
398 print()
389
399
390 if options.coverage:
400 if options.coverage:
391 from coverage import coverage
401 from coverage import coverage
392 cov = coverage(data_file='.coverage')
402 cov = coverage(data_file='.coverage')
393 cov.combine()
403 cov.combine()
394 cov.save()
404 cov.save()
395
405
396 # Coverage HTML report
406 # Coverage HTML report
397 if options.coverage == 'html':
407 if options.coverage == 'html':
398 html_dir = 'ipy_htmlcov'
408 html_dir = 'ipy_htmlcov'
399 shutil.rmtree(html_dir, ignore_errors=True)
409 shutil.rmtree(html_dir, ignore_errors=True)
400 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
410 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
401 sys.stdout.flush()
411 sys.stdout.flush()
402
412
403 # Custom HTML reporter to clean up module names.
413 # Custom HTML reporter to clean up module names.
404 from coverage.html import HtmlReporter
414 from coverage.html import HtmlReporter
405 class CustomHtmlReporter(HtmlReporter):
415 class CustomHtmlReporter(HtmlReporter):
406 def find_code_units(self, morfs):
416 def find_code_units(self, morfs):
407 super(CustomHtmlReporter, self).find_code_units(morfs)
417 super(CustomHtmlReporter, self).find_code_units(morfs)
408 for cu in self.code_units:
418 for cu in self.code_units:
409 nameparts = cu.name.split(os.sep)
419 nameparts = cu.name.split(os.sep)
410 if 'IPython' not in nameparts:
420 if 'IPython' not in nameparts:
411 continue
421 continue
412 ix = nameparts.index('IPython')
422 ix = nameparts.index('IPython')
413 cu.name = '.'.join(nameparts[ix:])
423 cu.name = '.'.join(nameparts[ix:])
414
424
415 # Reimplement the html_report method with our custom reporter
425 # Reimplement the html_report method with our custom reporter
416 cov._harvest_data()
426 cov._harvest_data()
417 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
427 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
418 html_title='IPython test coverage',
428 html_title='IPython test coverage',
419 )
429 )
420 reporter = CustomHtmlReporter(cov, cov.config)
430 reporter = CustomHtmlReporter(cov, cov.config)
421 reporter.report(None)
431 reporter.report(None)
422 print('done.')
432 print('done.')
423
433
424 # Coverage XML report
434 # Coverage XML report
425 elif options.coverage == 'xml':
435 elif options.coverage == 'xml':
426 cov.xml_report(outfile='ipy_coverage.xml')
436 cov.xml_report(outfile='ipy_coverage.xml')
427
437
428 if failed:
438 if failed:
429 # Ensure that our exit code indicates failure
439 # Ensure that our exit code indicates failure
430 sys.exit(1)
440 sys.exit(1)
431
441
432
442
433 def main():
443 def main():
434 # Arguments after -- should be passed through to nose. Argparse treats
444 # Arguments after -- should be passed through to nose. Argparse treats
435 # everything after -- as regular positional arguments, so we separate them
445 # everything after -- as regular positional arguments, so we separate them
436 # first.
446 # first.
437 try:
447 try:
438 ix = sys.argv.index('--')
448 ix = sys.argv.index('--')
439 except ValueError:
449 except ValueError:
440 to_parse = sys.argv[1:]
450 to_parse = sys.argv[1:]
441 extra_args = []
451 extra_args = []
442 else:
452 else:
443 to_parse = sys.argv[1:ix]
453 to_parse = sys.argv[1:ix]
444 extra_args = sys.argv[ix+1:]
454 extra_args = sys.argv[ix+1:]
445
455
446 parser = argparse.ArgumentParser(description='Run IPython test suite')
456 parser = argparse.ArgumentParser(description='Run IPython test suite')
447 parser.add_argument('testgroups', nargs='*',
457 parser.add_argument('testgroups', nargs='*',
448 help='Run specified groups of tests. If omitted, run '
458 help='Run specified groups of tests. If omitted, run '
449 'all tests.')
459 'all tests.')
450 parser.add_argument('--all', action='store_true',
460 parser.add_argument('--all', action='store_true',
451 help='Include slow tests not run by default.')
461 help='Include slow tests not run by default.')
452 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
462 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
453 help='Run test sections in parallel.')
463 help='Run test sections in parallel.')
454 parser.add_argument('--xunit', action='store_true',
464 parser.add_argument('--xunit', action='store_true',
455 help='Produce Xunit XML results')
465 help='Produce Xunit XML results')
456 parser.add_argument('--coverage', nargs='?', const=True, default=False,
466 parser.add_argument('--coverage', nargs='?', const=True, default=False,
457 help="Measure test coverage. Specify 'html' or "
467 help="Measure test coverage. Specify 'html' or "
458 "'xml' to get reports.")
468 "'xml' to get reports.")
459
469
460 options = parser.parse_args(to_parse)
470 options = parser.parse_args(to_parse)
461 options.extra_args = extra_args
471 options.extra_args = extra_args
462
472
463 run_iptestall(options)
473 run_iptestall(options)
464
474
465
475
466 if __name__ == '__main__':
476 if __name__ == '__main__':
467 main()
477 main()
General Comments 0
You need to be logged in to leave comments. Login now