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