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