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