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