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