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