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