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