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