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