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