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