##// END OF EJS Templates
BUG: test runner fails in Windows if filenames contain spaces.
Jonathan March -
Show More
@@ -1,559 +1,563 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
27
28 # Stdlib
28 # Stdlib
29 import glob
29 import glob
30 import os
30 import os
31 import os.path as path
31 import os.path as path
32 import signal
32 import signal
33 import sys
33 import sys
34 import subprocess
34 import subprocess
35 import tempfile
35 import tempfile
36 import time
36 import time
37 import warnings
37 import warnings
38
38
39 # Note: monkeypatch!
39 # Note: monkeypatch!
40 # We need to monkeypatch a small problem in nose itself first, before importing
40 # We need to monkeypatch a small problem in nose itself first, before importing
41 # it for actual use. This should get into nose upstream, but its release cycle
41 # it for actual use. This should get into nose upstream, but its release cycle
42 # is slow and we need it for our parametric tests to work correctly.
42 # is slow and we need it for our parametric tests to work correctly.
43 from IPython.testing import nosepatch
43 from IPython.testing import nosepatch
44
44
45 # Monkeypatch extra assert methods into nose.tools if they're not already there.
45 # Monkeypatch extra assert methods into nose.tools if they're not already there.
46 # This can be dropped once we no longer test on Python 2.6
46 # This can be dropped once we no longer test on Python 2.6
47 from IPython.testing import nose_assert_methods
47 from IPython.testing import nose_assert_methods
48
48
49 # Now, proceed to import nose itself
49 # Now, proceed to import nose itself
50 import nose.plugins.builtin
50 import nose.plugins.builtin
51 from nose.plugins.xunit import Xunit
51 from nose.plugins.xunit import Xunit
52 from nose import SkipTest
52 from nose import SkipTest
53 from nose.core import TestProgram
53 from nose.core import TestProgram
54
54
55 # Our own imports
55 # Our own imports
56 from IPython.utils.importstring import import_item
56 from IPython.utils.importstring import import_item
57 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
57 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
58 from IPython.utils.process import find_cmd, pycmd2argv
58 from IPython.utils.process import find_cmd, pycmd2argv
59 from IPython.utils.sysinfo import sys_info
59 from IPython.utils.sysinfo import sys_info
60 from IPython.utils.warn import warn
60 from IPython.utils.warn import warn
61
61
62 from IPython.testing import globalipapp
62 from IPython.testing import globalipapp
63 from IPython.testing.plugin.ipdoctest import IPythonDoctest
63 from IPython.testing.plugin.ipdoctest import IPythonDoctest
64 from IPython.external.decorators import KnownFailure, knownfailureif
64 from IPython.external.decorators import KnownFailure, knownfailureif
65
65
66 pjoin = path.join
66 pjoin = path.join
67
67
68
68
69 #-----------------------------------------------------------------------------
69 #-----------------------------------------------------------------------------
70 # Globals
70 # Globals
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72
72
73
73
74 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
75 # Warnings control
75 # Warnings control
76 #-----------------------------------------------------------------------------
76 #-----------------------------------------------------------------------------
77
77
78 # Twisted generates annoying warnings with Python 2.6, as will do other code
78 # Twisted generates annoying warnings with Python 2.6, as will do other code
79 # that imports 'sets' as of today
79 # that imports 'sets' as of today
80 warnings.filterwarnings('ignore', 'the sets module is deprecated',
80 warnings.filterwarnings('ignore', 'the sets module is deprecated',
81 DeprecationWarning )
81 DeprecationWarning )
82
82
83 # This one also comes from Twisted
83 # This one also comes from Twisted
84 warnings.filterwarnings('ignore', 'the sha module is deprecated',
84 warnings.filterwarnings('ignore', 'the sha module is deprecated',
85 DeprecationWarning)
85 DeprecationWarning)
86
86
87 # Wx on Fedora11 spits these out
87 # Wx on Fedora11 spits these out
88 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
88 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
89 UserWarning)
89 UserWarning)
90
90
91 # ------------------------------------------------------------------------------
91 # ------------------------------------------------------------------------------
92 # Monkeypatch Xunit to count known failures as skipped.
92 # Monkeypatch Xunit to count known failures as skipped.
93 # ------------------------------------------------------------------------------
93 # ------------------------------------------------------------------------------
94 def monkeypatch_xunit():
94 def monkeypatch_xunit():
95 try:
95 try:
96 knownfailureif(True)(lambda: None)()
96 knownfailureif(True)(lambda: None)()
97 except Exception as e:
97 except Exception as e:
98 KnownFailureTest = type(e)
98 KnownFailureTest = type(e)
99
99
100 def addError(self, test, err, capt=None):
100 def addError(self, test, err, capt=None):
101 if issubclass(err[0], KnownFailureTest):
101 if issubclass(err[0], KnownFailureTest):
102 err = (SkipTest,) + err[1:]
102 err = (SkipTest,) + err[1:]
103 return self.orig_addError(test, err, capt)
103 return self.orig_addError(test, err, capt)
104
104
105 Xunit.orig_addError = Xunit.addError
105 Xunit.orig_addError = Xunit.addError
106 Xunit.addError = addError
106 Xunit.addError = addError
107
107
108 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
109 # Logic for skipping doctests
109 # Logic for skipping doctests
110 #-----------------------------------------------------------------------------
110 #-----------------------------------------------------------------------------
111 def extract_version(mod):
111 def extract_version(mod):
112 return mod.__version__
112 return mod.__version__
113
113
114 def test_for(item, min_version=None, callback=extract_version):
114 def test_for(item, min_version=None, callback=extract_version):
115 """Test to see if item is importable, and optionally check against a minimum
115 """Test to see if item is importable, and optionally check against a minimum
116 version.
116 version.
117
117
118 If min_version is given, the default behavior is to check against the
118 If min_version is given, the default behavior is to check against the
119 `__version__` attribute of the item, but specifying `callback` allows you to
119 `__version__` attribute of the item, but specifying `callback` allows you to
120 extract the value you are interested in. e.g::
120 extract the value you are interested in. e.g::
121
121
122 In [1]: import sys
122 In [1]: import sys
123
123
124 In [2]: from IPython.testing.iptest import test_for
124 In [2]: from IPython.testing.iptest import test_for
125
125
126 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
126 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
127 Out[3]: True
127 Out[3]: True
128
128
129 """
129 """
130 try:
130 try:
131 check = import_item(item)
131 check = import_item(item)
132 except (ImportError, RuntimeError):
132 except (ImportError, RuntimeError):
133 # GTK reports Runtime error if it can't be initialized even if it's
133 # GTK reports Runtime error if it can't be initialized even if it's
134 # importable.
134 # importable.
135 return False
135 return False
136 else:
136 else:
137 if min_version:
137 if min_version:
138 if callback:
138 if callback:
139 # extra processing step to get version to compare
139 # extra processing step to get version to compare
140 check = callback(check)
140 check = callback(check)
141
141
142 return check >= min_version
142 return check >= min_version
143 else:
143 else:
144 return True
144 return True
145
145
146 # Global dict where we can store information on what we have and what we don't
146 # Global dict where we can store information on what we have and what we don't
147 # have available at test run time
147 # have available at test run time
148 have = {}
148 have = {}
149
149
150 have['curses'] = test_for('_curses')
150 have['curses'] = test_for('_curses')
151 have['matplotlib'] = test_for('matplotlib')
151 have['matplotlib'] = test_for('matplotlib')
152 have['numpy'] = test_for('numpy')
152 have['numpy'] = test_for('numpy')
153 have['pexpect'] = test_for('IPython.external.pexpect')
153 have['pexpect'] = test_for('IPython.external.pexpect')
154 have['pymongo'] = test_for('pymongo')
154 have['pymongo'] = test_for('pymongo')
155 have['pygments'] = test_for('pygments')
155 have['pygments'] = test_for('pygments')
156 have['qt'] = test_for('IPython.external.qt')
156 have['qt'] = test_for('IPython.external.qt')
157 have['rpy2'] = test_for('rpy2')
157 have['rpy2'] = test_for('rpy2')
158 have['sqlite3'] = test_for('sqlite3')
158 have['sqlite3'] = test_for('sqlite3')
159 have['cython'] = test_for('Cython')
159 have['cython'] = test_for('Cython')
160 have['oct2py'] = test_for('oct2py')
160 have['oct2py'] = test_for('oct2py')
161 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
161 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
162 have['wx'] = test_for('wx')
162 have['wx'] = test_for('wx')
163 have['wx.aui'] = test_for('wx.aui')
163 have['wx.aui'] = test_for('wx.aui')
164
164
165 if os.name == 'nt':
165 if os.name == 'nt':
166 min_zmq = (2,1,7)
166 min_zmq = (2,1,7)
167 else:
167 else:
168 min_zmq = (2,1,4)
168 min_zmq = (2,1,4)
169
169
170 def version_tuple(mod):
170 def version_tuple(mod):
171 "turn '2.1.9' into (2,1,9), and '2.1dev' into (2,1,999)"
171 "turn '2.1.9' into (2,1,9), and '2.1dev' into (2,1,999)"
172 # turn 'dev' into 999, because Python3 rejects str-int comparisons
172 # turn 'dev' into 999, because Python3 rejects str-int comparisons
173 vs = mod.__version__.replace('dev', '.999')
173 vs = mod.__version__.replace('dev', '.999')
174 tup = tuple([int(v) for v in vs.split('.') ])
174 tup = tuple([int(v) for v in vs.split('.') ])
175 return tup
175 return tup
176
176
177 have['zmq'] = test_for('zmq', min_zmq, version_tuple)
177 have['zmq'] = test_for('zmq', min_zmq, version_tuple)
178
178
179 #-----------------------------------------------------------------------------
179 #-----------------------------------------------------------------------------
180 # Functions and classes
180 # Functions and classes
181 #-----------------------------------------------------------------------------
181 #-----------------------------------------------------------------------------
182
182
183 def report():
183 def report():
184 """Return a string with a summary report of test-related variables."""
184 """Return a string with a summary report of test-related variables."""
185
185
186 out = [ sys_info(), '\n']
186 out = [ sys_info(), '\n']
187
187
188 avail = []
188 avail = []
189 not_avail = []
189 not_avail = []
190
190
191 for k, is_avail in have.items():
191 for k, is_avail in have.items():
192 if is_avail:
192 if is_avail:
193 avail.append(k)
193 avail.append(k)
194 else:
194 else:
195 not_avail.append(k)
195 not_avail.append(k)
196
196
197 if avail:
197 if avail:
198 out.append('\nTools and libraries available at test time:\n')
198 out.append('\nTools and libraries available at test time:\n')
199 avail.sort()
199 avail.sort()
200 out.append(' ' + ' '.join(avail)+'\n')
200 out.append(' ' + ' '.join(avail)+'\n')
201
201
202 if not_avail:
202 if not_avail:
203 out.append('\nTools and libraries NOT available at test time:\n')
203 out.append('\nTools and libraries NOT available at test time:\n')
204 not_avail.sort()
204 not_avail.sort()
205 out.append(' ' + ' '.join(not_avail)+'\n')
205 out.append(' ' + ' '.join(not_avail)+'\n')
206
206
207 return ''.join(out)
207 return ''.join(out)
208
208
209
209
210 def make_exclude():
210 def make_exclude():
211 """Make patterns of modules and packages to exclude from testing.
211 """Make patterns of modules and packages to exclude from testing.
212
212
213 For the IPythonDoctest plugin, we need to exclude certain patterns that
213 For the IPythonDoctest plugin, we need to exclude certain patterns that
214 cause testing problems. We should strive to minimize the number of
214 cause testing problems. We should strive to minimize the number of
215 skipped modules, since this means untested code.
215 skipped modules, since this means untested code.
216
216
217 These modules and packages will NOT get scanned by nose at all for tests.
217 These modules and packages will NOT get scanned by nose at all for tests.
218 """
218 """
219 # Simple utility to make IPython paths more readably, we need a lot of
219 # Simple utility to make IPython paths more readably, we need a lot of
220 # these below
220 # these below
221 ipjoin = lambda *paths: pjoin('IPython', *paths)
221 ipjoin = lambda *paths: pjoin('IPython', *paths)
222
222
223 exclusions = [ipjoin('external'),
223 exclusions = [ipjoin('external'),
224 ipjoin('quarantine'),
224 ipjoin('quarantine'),
225 ipjoin('deathrow'),
225 ipjoin('deathrow'),
226 # This guy is probably attic material
226 # This guy is probably attic material
227 ipjoin('testing', 'mkdoctests'),
227 ipjoin('testing', 'mkdoctests'),
228 # Testing inputhook will need a lot of thought, to figure out
228 # Testing inputhook will need a lot of thought, to figure out
229 # how to have tests that don't lock up with the gui event
229 # how to have tests that don't lock up with the gui event
230 # loops in the picture
230 # loops in the picture
231 ipjoin('lib', 'inputhook'),
231 ipjoin('lib', 'inputhook'),
232 # Config files aren't really importable stand-alone
232 # Config files aren't really importable stand-alone
233 ipjoin('config', 'profile'),
233 ipjoin('config', 'profile'),
234 ]
234 ]
235 if not have['sqlite3']:
235 if not have['sqlite3']:
236 exclusions.append(ipjoin('core', 'tests', 'test_history'))
236 exclusions.append(ipjoin('core', 'tests', 'test_history'))
237 exclusions.append(ipjoin('core', 'history'))
237 exclusions.append(ipjoin('core', 'history'))
238 if not have['wx']:
238 if not have['wx']:
239 exclusions.append(ipjoin('lib', 'inputhookwx'))
239 exclusions.append(ipjoin('lib', 'inputhookwx'))
240
240
241 # FIXME: temporarily disable autoreload tests, as they can produce
241 # FIXME: temporarily disable autoreload tests, as they can produce
242 # spurious failures in subsequent tests (cythonmagic).
242 # spurious failures in subsequent tests (cythonmagic).
243 exclusions.append(ipjoin('extensions', 'autoreload'))
243 exclusions.append(ipjoin('extensions', 'autoreload'))
244 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
244 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
245
245
246 # We do this unconditionally, so that the test suite doesn't import
246 # We do this unconditionally, so that the test suite doesn't import
247 # gtk, changing the default encoding and masking some unicode bugs.
247 # gtk, changing the default encoding and masking some unicode bugs.
248 exclusions.append(ipjoin('lib', 'inputhookgtk'))
248 exclusions.append(ipjoin('lib', 'inputhookgtk'))
249 exclusions.append(ipjoin('zmq', 'gui', 'gtkembed'))
249 exclusions.append(ipjoin('zmq', 'gui', 'gtkembed'))
250
250
251 # These have to be skipped on win32 because the use echo, rm, cd, etc.
251 # These have to be skipped on win32 because the use echo, rm, cd, etc.
252 # See ticket https://github.com/ipython/ipython/issues/87
252 # See ticket https://github.com/ipython/ipython/issues/87
253 if sys.platform == 'win32':
253 if sys.platform == 'win32':
254 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
254 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
255 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
255 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
256
256
257 if not have['pexpect']:
257 if not have['pexpect']:
258 exclusions.extend([ipjoin('scripts', 'irunner'),
258 exclusions.extend([ipjoin('scripts', 'irunner'),
259 ipjoin('lib', 'irunner'),
259 ipjoin('lib', 'irunner'),
260 ipjoin('lib', 'tests', 'test_irunner'),
260 ipjoin('lib', 'tests', 'test_irunner'),
261 ipjoin('frontend', 'terminal', 'console'),
261 ipjoin('frontend', 'terminal', 'console'),
262 ])
262 ])
263
263
264 if not have['zmq']:
264 if not have['zmq']:
265 exclusions.append(ipjoin('zmq'))
265 exclusions.append(ipjoin('zmq'))
266 exclusions.append(ipjoin('frontend', 'qt'))
266 exclusions.append(ipjoin('frontend', 'qt'))
267 exclusions.append(ipjoin('frontend', 'html'))
267 exclusions.append(ipjoin('frontend', 'html'))
268 exclusions.append(ipjoin('frontend', 'consoleapp.py'))
268 exclusions.append(ipjoin('frontend', 'consoleapp.py'))
269 exclusions.append(ipjoin('frontend', 'terminal', 'console'))
269 exclusions.append(ipjoin('frontend', 'terminal', 'console'))
270 exclusions.append(ipjoin('parallel'))
270 exclusions.append(ipjoin('parallel'))
271 elif not have['qt'] or not have['pygments']:
271 elif not have['qt'] or not have['pygments']:
272 exclusions.append(ipjoin('frontend', 'qt'))
272 exclusions.append(ipjoin('frontend', 'qt'))
273
273
274 if not have['pymongo']:
274 if not have['pymongo']:
275 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
275 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
276 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
276 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
277
277
278 if not have['matplotlib']:
278 if not have['matplotlib']:
279 exclusions.extend([ipjoin('core', 'pylabtools'),
279 exclusions.extend([ipjoin('core', 'pylabtools'),
280 ipjoin('core', 'tests', 'test_pylabtools'),
280 ipjoin('core', 'tests', 'test_pylabtools'),
281 ipjoin('zmq', 'pylab'),
281 ipjoin('zmq', 'pylab'),
282 ])
282 ])
283
283
284 if not have['cython']:
284 if not have['cython']:
285 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
285 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
286 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
286 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
287
287
288 if not have['oct2py']:
288 if not have['oct2py']:
289 exclusions.extend([ipjoin('extensions', 'octavemagic')])
289 exclusions.extend([ipjoin('extensions', 'octavemagic')])
290 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
290 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
291
291
292 if not have['tornado']:
292 if not have['tornado']:
293 exclusions.append(ipjoin('frontend', 'html'))
293 exclusions.append(ipjoin('frontend', 'html'))
294
294
295 if not have['rpy2'] or not have['numpy']:
295 if not have['rpy2'] or not have['numpy']:
296 exclusions.append(ipjoin('extensions', 'rmagic'))
296 exclusions.append(ipjoin('extensions', 'rmagic'))
297 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
297 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
298
298
299 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
299 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
300 if sys.platform == 'win32':
300 if sys.platform == 'win32':
301 exclusions = [s.replace('\\','\\\\') for s in exclusions]
301 exclusions = [s.replace('\\','\\\\') for s in exclusions]
302
302
303 # check for any exclusions that don't seem to exist:
303 # check for any exclusions that don't seem to exist:
304 parent, _ = os.path.split(get_ipython_package_dir())
304 parent, _ = os.path.split(get_ipython_package_dir())
305 for exclusion in exclusions:
305 for exclusion in exclusions:
306 if exclusion.endswith(('deathrow', 'quarantine')):
306 if exclusion.endswith(('deathrow', 'quarantine')):
307 # ignore deathrow/quarantine, which exist in dev, but not install
307 # ignore deathrow/quarantine, which exist in dev, but not install
308 continue
308 continue
309 fullpath = pjoin(parent, exclusion)
309 fullpath = pjoin(parent, exclusion)
310 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
310 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
311 warn("Excluding nonexistent file: %r\n" % exclusion)
311 warn("Excluding nonexistent file: %r\n" % exclusion)
312
312
313 return exclusions
313 return exclusions
314
314
315
315
316 class IPTester(object):
316 class IPTester(object):
317 """Call that calls iptest or trial in a subprocess.
317 """Call that calls iptest or trial in a subprocess.
318 """
318 """
319 #: string, name of test runner that will be called
319 #: string, name of test runner that will be called
320 runner = None
320 runner = None
321 #: list, parameters for test runner
321 #: list, parameters for test runner
322 params = None
322 params = None
323 #: list, arguments of system call to be made to call test runner
323 #: list, arguments of system call to be made to call test runner
324 call_args = None
324 call_args = None
325 #: list, process ids of subprocesses we start (for cleanup)
325 #: list, process ids of subprocesses we start (for cleanup)
326 pids = None
326 pids = None
327 #: str, coverage xml output file
327 #: str, coverage xml output file
328 coverage_xml = None
328 coverage_xml = None
329
329
330 def __init__(self, runner='iptest', params=None):
330 def __init__(self, runner='iptest', params=None):
331 """Create new test runner."""
331 """Create new test runner."""
332 p = os.path
332 p = os.path
333 if runner == 'iptest':
333 if runner == 'iptest':
334 iptest_app = get_ipython_module_path('IPython.testing.iptest')
334 iptest_app = get_ipython_module_path('IPython.testing.iptest')
335 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
335 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
336 else:
336 else:
337 raise Exception('Not a valid test runner: %s' % repr(runner))
337 raise Exception('Not a valid test runner: %s' % repr(runner))
338 if params is None:
338 if params is None:
339 params = []
339 params = []
340 if isinstance(params, str):
340 if isinstance(params, str):
341 params = [params]
341 params = [params]
342 self.params = params
342 self.params = params
343
343
344 # Assemble call
344 # Assemble call
345 self.call_args = self.runner+self.params
345 self.call_args = self.runner+self.params
346
346
347 # Find the section we're testing (IPython.foo)
347 # Find the section we're testing (IPython.foo)
348 for sect in self.params:
348 for sect in self.params:
349 if sect.startswith('IPython'): break
349 if sect.startswith('IPython'): break
350 else:
350 else:
351 raise ValueError("Section not found", self.params)
351 raise ValueError("Section not found", self.params)
352
352
353 if '--with-xunit' in self.call_args:
353 if '--with-xunit' in self.call_args:
354 self.call_args.append('--xunit-file=%s' % path.abspath(sect+'.xunit.xml'))
354 self.call_args.append('--xunit-file=%s' % path.abspath(sect+'.xunit.xml'))
355
355
356 if '--with-xml-coverage' in self.call_args:
356 if '--with-xml-coverage' in self.call_args:
357 self.coverage_xml = path.abspath(sect+".coverage.xml")
357 self.coverage_xml = path.abspath(sect+".coverage.xml")
358 self.call_args.remove('--with-xml-coverage')
358 self.call_args.remove('--with-xml-coverage')
359 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
359 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
360
360
361 # Store pids of anything we start to clean up on deletion, if possible
361 # Store pids of anything we start to clean up on deletion, if possible
362 # (on posix only, since win32 has no os.kill)
362 # (on posix only, since win32 has no os.kill)
363 self.pids = []
363 self.pids = []
364
364
365 if sys.platform == 'win32':
365 if sys.platform == 'win32':
366 def _run_cmd(self):
366 def _run_cmd(self):
367 # On Windows, use os.system instead of subprocess.call, because I
367 # On Windows, use os.system instead of subprocess.call, because I
368 # was having problems with subprocess and I just don't know enough
368 # was having problems with subprocess and I just don't know enough
369 # about win32 to debug this reliably. Os.system may be the 'old
369 # about win32 to debug this reliably. Os.system may be the 'old
370 # fashioned' way to do it, but it works just fine. If someone
370 # fashioned' way to do it, but it works just fine. If someone
371 # later can clean this up that's fine, as long as the tests run
371 # later can clean this up that's fine, as long as the tests run
372 # reliably in win32.
372 # reliably in win32.
373 # What types of problems are you having. They may be related to
373 # What types of problems are you having. They may be related to
374 # running Python in unboffered mode. BG.
374 # running Python in unboffered mode. BG.
375 for ndx, arg in enumerate(self.call_args):
376 # Enclose in quotes if necessary and legal
377 if ' ' in arg and os.path.isfile(arg) and arg[0] != '"':
378 self.call_args[ndx] = '"%s"' % arg
375 return os.system(' '.join(self.call_args))
379 return os.system(' '.join(self.call_args))
376 else:
380 else:
377 def _run_cmd(self):
381 def _run_cmd(self):
378 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
382 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
379 subp = subprocess.Popen(self.call_args)
383 subp = subprocess.Popen(self.call_args)
380 self.pids.append(subp.pid)
384 self.pids.append(subp.pid)
381 # If this fails, the pid will be left in self.pids and cleaned up
385 # If this fails, the pid will be left in self.pids and cleaned up
382 # later, but if the wait call succeeds, then we can clear the
386 # later, but if the wait call succeeds, then we can clear the
383 # stored pid.
387 # stored pid.
384 retcode = subp.wait()
388 retcode = subp.wait()
385 self.pids.pop()
389 self.pids.pop()
386 return retcode
390 return retcode
387
391
388 def run(self):
392 def run(self):
389 """Run the stored commands"""
393 """Run the stored commands"""
390 try:
394 try:
391 retcode = self._run_cmd()
395 retcode = self._run_cmd()
392 except:
396 except:
393 import traceback
397 import traceback
394 traceback.print_exc()
398 traceback.print_exc()
395 return 1 # signal failure
399 return 1 # signal failure
396
400
397 if self.coverage_xml:
401 if self.coverage_xml:
398 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
402 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
399 return retcode
403 return retcode
400
404
401 def __del__(self):
405 def __del__(self):
402 """Cleanup on exit by killing any leftover processes."""
406 """Cleanup on exit by killing any leftover processes."""
403
407
404 if not hasattr(os, 'kill'):
408 if not hasattr(os, 'kill'):
405 return
409 return
406
410
407 for pid in self.pids:
411 for pid in self.pids:
408 try:
412 try:
409 print 'Cleaning stale PID:', pid
413 print 'Cleaning stale PID:', pid
410 os.kill(pid, signal.SIGKILL)
414 os.kill(pid, signal.SIGKILL)
411 except OSError:
415 except OSError:
412 # This is just a best effort, if we fail or the process was
416 # This is just a best effort, if we fail or the process was
413 # really gone, ignore it.
417 # really gone, ignore it.
414 pass
418 pass
415
419
416
420
417 def make_runners():
421 def make_runners():
418 """Define the top-level packages that need to be tested.
422 """Define the top-level packages that need to be tested.
419 """
423 """
420
424
421 # Packages to be tested via nose, that only depend on the stdlib
425 # Packages to be tested via nose, that only depend on the stdlib
422 nose_pkg_names = ['config', 'core', 'extensions', 'frontend', 'lib',
426 nose_pkg_names = ['config', 'core', 'extensions', 'frontend', 'lib',
423 'scripts', 'testing', 'utils', 'nbformat' ]
427 'scripts', 'testing', 'utils', 'nbformat' ]
424
428
425 if have['zmq']:
429 if have['zmq']:
426 nose_pkg_names.append('zmq')
430 nose_pkg_names.append('zmq')
427 nose_pkg_names.append('parallel')
431 nose_pkg_names.append('parallel')
428
432
429 # For debugging this code, only load quick stuff
433 # For debugging this code, only load quick stuff
430 #nose_pkg_names = ['core', 'extensions'] # dbg
434 #nose_pkg_names = ['core', 'extensions'] # dbg
431
435
432 # Make fully qualified package names prepending 'IPython.' to our name lists
436 # Make fully qualified package names prepending 'IPython.' to our name lists
433 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
437 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
434
438
435 # Make runners
439 # Make runners
436 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
440 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
437
441
438 return runners
442 return runners
439
443
440
444
441 def run_iptest():
445 def run_iptest():
442 """Run the IPython test suite using nose.
446 """Run the IPython test suite using nose.
443
447
444 This function is called when this script is **not** called with the form
448 This function is called when this script is **not** called with the form
445 `iptest all`. It simply calls nose with appropriate command line flags
449 `iptest all`. It simply calls nose with appropriate command line flags
446 and accepts all of the standard nose arguments.
450 and accepts all of the standard nose arguments.
447 """
451 """
448 # Apply our monkeypatch to Xunit
452 # Apply our monkeypatch to Xunit
449 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
453 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
450 monkeypatch_xunit()
454 monkeypatch_xunit()
451
455
452 warnings.filterwarnings('ignore',
456 warnings.filterwarnings('ignore',
453 'This will be removed soon. Use IPython.testing.util instead')
457 'This will be removed soon. Use IPython.testing.util instead')
454
458
455 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
459 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
456
460
457 '--with-ipdoctest',
461 '--with-ipdoctest',
458 '--ipdoctest-tests','--ipdoctest-extension=txt',
462 '--ipdoctest-tests','--ipdoctest-extension=txt',
459
463
460 # We add --exe because of setuptools' imbecility (it
464 # We add --exe because of setuptools' imbecility (it
461 # blindly does chmod +x on ALL files). Nose does the
465 # blindly does chmod +x on ALL files). Nose does the
462 # right thing and it tries to avoid executables,
466 # right thing and it tries to avoid executables,
463 # setuptools unfortunately forces our hand here. This
467 # setuptools unfortunately forces our hand here. This
464 # has been discussed on the distutils list and the
468 # has been discussed on the distutils list and the
465 # setuptools devs refuse to fix this problem!
469 # setuptools devs refuse to fix this problem!
466 '--exe',
470 '--exe',
467 ]
471 ]
468
472
469 if nose.__version__ >= '0.11':
473 if nose.__version__ >= '0.11':
470 # I don't fully understand why we need this one, but depending on what
474 # I don't fully understand why we need this one, but depending on what
471 # directory the test suite is run from, if we don't give it, 0 tests
475 # directory the test suite is run from, if we don't give it, 0 tests
472 # get run. Specifically, if the test suite is run from the source dir
476 # get run. Specifically, if the test suite is run from the source dir
473 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
477 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
474 # even if the same call done in this directory works fine). It appears
478 # even if the same call done in this directory works fine). It appears
475 # that if the requested package is in the current dir, nose bails early
479 # that if the requested package is in the current dir, nose bails early
476 # by default. Since it's otherwise harmless, leave it in by default
480 # by default. Since it's otherwise harmless, leave it in by default
477 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
481 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
478 argv.append('--traverse-namespace')
482 argv.append('--traverse-namespace')
479
483
480 # use our plugin for doctesting. It will remove the standard doctest plugin
484 # use our plugin for doctesting. It will remove the standard doctest plugin
481 # if it finds it enabled
485 # if it finds it enabled
482 plugins = [IPythonDoctest(make_exclude()), KnownFailure()]
486 plugins = [IPythonDoctest(make_exclude()), KnownFailure()]
483 # We need a global ipython running in this process
487 # We need a global ipython running in this process
484 globalipapp.start_ipython()
488 globalipapp.start_ipython()
485 # Now nose can run
489 # Now nose can run
486 TestProgram(argv=argv, addplugins=plugins)
490 TestProgram(argv=argv, addplugins=plugins)
487
491
488
492
489 def run_iptestall():
493 def run_iptestall():
490 """Run the entire IPython test suite by calling nose and trial.
494 """Run the entire IPython test suite by calling nose and trial.
491
495
492 This function constructs :class:`IPTester` instances for all IPython
496 This function constructs :class:`IPTester` instances for all IPython
493 modules and package and then runs each of them. This causes the modules
497 modules and package and then runs each of them. This causes the modules
494 and packages of IPython to be tested each in their own subprocess using
498 and packages of IPython to be tested each in their own subprocess using
495 nose.
499 nose.
496 """
500 """
497
501
498 runners = make_runners()
502 runners = make_runners()
499
503
500 # Run the test runners in a temporary dir so we can nuke it when finished
504 # Run the test runners in a temporary dir so we can nuke it when finished
501 # to clean up any junk files left over by accident. This also makes it
505 # to clean up any junk files left over by accident. This also makes it
502 # robust against being run in non-writeable directories by mistake, as the
506 # robust against being run in non-writeable directories by mistake, as the
503 # temp dir will always be user-writeable.
507 # temp dir will always be user-writeable.
504 curdir = os.getcwdu()
508 curdir = os.getcwdu()
505 testdir = tempfile.gettempdir()
509 testdir = tempfile.gettempdir()
506 os.chdir(testdir)
510 os.chdir(testdir)
507
511
508 # Run all test runners, tracking execution time
512 # Run all test runners, tracking execution time
509 failed = []
513 failed = []
510 t_start = time.time()
514 t_start = time.time()
511 try:
515 try:
512 for (name, runner) in runners:
516 for (name, runner) in runners:
513 print '*'*70
517 print '*'*70
514 print 'IPython test group:',name
518 print 'IPython test group:',name
515 res = runner.run()
519 res = runner.run()
516 if res:
520 if res:
517 failed.append( (name, runner) )
521 failed.append( (name, runner) )
518 finally:
522 finally:
519 os.chdir(curdir)
523 os.chdir(curdir)
520 t_end = time.time()
524 t_end = time.time()
521 t_tests = t_end - t_start
525 t_tests = t_end - t_start
522 nrunners = len(runners)
526 nrunners = len(runners)
523 nfail = len(failed)
527 nfail = len(failed)
524 # summarize results
528 # summarize results
525 print
529 print
526 print '*'*70
530 print '*'*70
527 print 'Test suite completed for system with the following information:'
531 print 'Test suite completed for system with the following information:'
528 print report()
532 print report()
529 print 'Ran %s test groups in %.3fs' % (nrunners, t_tests)
533 print 'Ran %s test groups in %.3fs' % (nrunners, t_tests)
530 print
534 print
531 print 'Status:'
535 print 'Status:'
532 if not failed:
536 if not failed:
533 print 'OK'
537 print 'OK'
534 else:
538 else:
535 # If anything went wrong, point out what command to rerun manually to
539 # If anything went wrong, point out what command to rerun manually to
536 # see the actual errors and individual summary
540 # see the actual errors and individual summary
537 print 'ERROR - %s out of %s test groups failed.' % (nfail, nrunners)
541 print 'ERROR - %s out of %s test groups failed.' % (nfail, nrunners)
538 for name, failed_runner in failed:
542 for name, failed_runner in failed:
539 print '-'*40
543 print '-'*40
540 print 'Runner failed:',name
544 print 'Runner failed:',name
541 print 'You may wish to rerun this one individually, with:'
545 print 'You may wish to rerun this one individually, with:'
542 print ' '.join(failed_runner.call_args)
546 print ' '.join(failed_runner.call_args)
543 print
547 print
544 # Ensure that our exit code indicates failure
548 # Ensure that our exit code indicates failure
545 sys.exit(1)
549 sys.exit(1)
546
550
547
551
548 def main():
552 def main():
549 for arg in sys.argv[1:]:
553 for arg in sys.argv[1:]:
550 if arg.startswith('IPython'):
554 if arg.startswith('IPython'):
551 # This is in-process
555 # This is in-process
552 run_iptest()
556 run_iptest()
553 else:
557 else:
554 # This starts subprocesses
558 # This starts subprocesses
555 run_iptestall()
559 run_iptestall()
556
560
557
561
558 if __name__ == '__main__':
562 if __name__ == '__main__':
559 main()
563 main()
General Comments 0
You need to be logged in to leave comments. Login now