##// END OF EJS Templates
use \0 instead of 'wake up' to finish output...
Min RK -
Show More
@@ -1,519 +1,519 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 # Copyright (c) IPython Development Team.
18 18 # Distributed under the terms of the Modified BSD License.
19 19
20 20 from __future__ import print_function
21 21
22 22 import glob
23 23 from io import BytesIO
24 24 import os
25 25 import os.path as path
26 26 import sys
27 27 from threading import Thread, Lock, Event
28 28 import warnings
29 29
30 30 import nose.plugins.builtin
31 31 from nose.plugins.xunit import Xunit
32 32 from nose import SkipTest
33 33 from nose.core import TestProgram
34 34 from nose.plugins import Plugin
35 35 from nose.util import safe_str
36 36
37 37 from IPython.utils.process import is_cmd_found
38 38 from IPython.utils.py3compat import bytes_to_str
39 39 from IPython.utils.importstring import import_item
40 40 from IPython.testing.plugin.ipdoctest import IPythonDoctest
41 41 from IPython.external.decorators import KnownFailure, knownfailureif
42 42
43 43 pjoin = path.join
44 44
45 45
46 46 #-----------------------------------------------------------------------------
47 47 # Globals
48 48 #-----------------------------------------------------------------------------
49 49
50 50
51 51 #-----------------------------------------------------------------------------
52 52 # Warnings control
53 53 #-----------------------------------------------------------------------------
54 54
55 55 # Twisted generates annoying warnings with Python 2.6, as will do other code
56 56 # that imports 'sets' as of today
57 57 warnings.filterwarnings('ignore', 'the sets module is deprecated',
58 58 DeprecationWarning )
59 59
60 60 # This one also comes from Twisted
61 61 warnings.filterwarnings('ignore', 'the sha module is deprecated',
62 62 DeprecationWarning)
63 63
64 64 # Wx on Fedora11 spits these out
65 65 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
66 66 UserWarning)
67 67
68 68 # ------------------------------------------------------------------------------
69 69 # Monkeypatch Xunit to count known failures as skipped.
70 70 # ------------------------------------------------------------------------------
71 71 def monkeypatch_xunit():
72 72 try:
73 73 knownfailureif(True)(lambda: None)()
74 74 except Exception as e:
75 75 KnownFailureTest = type(e)
76 76
77 77 def addError(self, test, err, capt=None):
78 78 if issubclass(err[0], KnownFailureTest):
79 79 err = (SkipTest,) + err[1:]
80 80 return self.orig_addError(test, err, capt)
81 81
82 82 Xunit.orig_addError = Xunit.addError
83 83 Xunit.addError = addError
84 84
85 85 #-----------------------------------------------------------------------------
86 86 # Check which dependencies are installed and greater than minimum version.
87 87 #-----------------------------------------------------------------------------
88 88 def extract_version(mod):
89 89 return mod.__version__
90 90
91 91 def test_for(item, min_version=None, callback=extract_version):
92 92 """Test to see if item is importable, and optionally check against a minimum
93 93 version.
94 94
95 95 If min_version is given, the default behavior is to check against the
96 96 `__version__` attribute of the item, but specifying `callback` allows you to
97 97 extract the value you are interested in. e.g::
98 98
99 99 In [1]: import sys
100 100
101 101 In [2]: from IPython.testing.iptest import test_for
102 102
103 103 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
104 104 Out[3]: True
105 105
106 106 """
107 107 try:
108 108 check = import_item(item)
109 109 except (ImportError, RuntimeError):
110 110 # GTK reports Runtime error if it can't be initialized even if it's
111 111 # importable.
112 112 return False
113 113 else:
114 114 if min_version:
115 115 if callback:
116 116 # extra processing step to get version to compare
117 117 check = callback(check)
118 118
119 119 return check >= min_version
120 120 else:
121 121 return True
122 122
123 123 # Global dict where we can store information on what we have and what we don't
124 124 # have available at test run time
125 125 have = {}
126 126
127 127 have['curses'] = test_for('_curses')
128 128 have['matplotlib'] = test_for('matplotlib')
129 129 have['numpy'] = test_for('numpy')
130 130 have['pexpect'] = test_for('IPython.external.pexpect')
131 131 have['pymongo'] = test_for('pymongo')
132 132 have['pygments'] = test_for('pygments')
133 133 have['qt'] = test_for('IPython.external.qt')
134 134 have['sqlite3'] = test_for('sqlite3')
135 135 have['tornado'] = test_for('tornado.version_info', (4,0), callback=None)
136 136 have['jinja2'] = test_for('jinja2')
137 137 have['mistune'] = test_for('mistune')
138 138 have['requests'] = test_for('requests')
139 139 have['sphinx'] = test_for('sphinx')
140 140 have['jsonschema'] = test_for('jsonschema')
141 141 have['terminado'] = test_for('terminado')
142 142 have['casperjs'] = is_cmd_found('casperjs')
143 143 have['phantomjs'] = is_cmd_found('phantomjs')
144 144 have['slimerjs'] = is_cmd_found('slimerjs')
145 145
146 146 min_zmq = (2,1,11)
147 147
148 148 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
149 149
150 150 #-----------------------------------------------------------------------------
151 151 # Test suite definitions
152 152 #-----------------------------------------------------------------------------
153 153
154 154 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
155 155 'extensions', 'lib', 'terminal', 'testing', 'utils',
156 156 'nbformat', 'qt', 'html', 'nbconvert'
157 157 ]
158 158
159 159 class TestSection(object):
160 160 def __init__(self, name, includes):
161 161 self.name = name
162 162 self.includes = includes
163 163 self.excludes = []
164 164 self.dependencies = []
165 165 self.enabled = True
166 166
167 167 def exclude(self, module):
168 168 if not module.startswith('IPython'):
169 169 module = self.includes[0] + "." + module
170 170 self.excludes.append(module.replace('.', os.sep))
171 171
172 172 def requires(self, *packages):
173 173 self.dependencies.extend(packages)
174 174
175 175 @property
176 176 def will_run(self):
177 177 return self.enabled and all(have[p] for p in self.dependencies)
178 178
179 179 # Name -> (include, exclude, dependencies_met)
180 180 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
181 181
182 182 # Exclusions and dependencies
183 183 # ---------------------------
184 184
185 185 # core:
186 186 sec = test_sections['core']
187 187 if not have['sqlite3']:
188 188 sec.exclude('tests.test_history')
189 189 sec.exclude('history')
190 190 if not have['matplotlib']:
191 191 sec.exclude('pylabtools'),
192 192 sec.exclude('tests.test_pylabtools')
193 193
194 194 # lib:
195 195 sec = test_sections['lib']
196 196 if not have['zmq']:
197 197 sec.exclude('kernel')
198 198 # We do this unconditionally, so that the test suite doesn't import
199 199 # gtk, changing the default encoding and masking some unicode bugs.
200 200 sec.exclude('inputhookgtk')
201 201 # We also do this unconditionally, because wx can interfere with Unix signals.
202 202 # There are currently no tests for it anyway.
203 203 sec.exclude('inputhookwx')
204 204 # Testing inputhook will need a lot of thought, to figure out
205 205 # how to have tests that don't lock up with the gui event
206 206 # loops in the picture
207 207 sec.exclude('inputhook')
208 208
209 209 # testing:
210 210 sec = test_sections['testing']
211 211 # These have to be skipped on win32 because they use echo, rm, cd, etc.
212 212 # See ticket https://github.com/ipython/ipython/issues/87
213 213 if sys.platform == 'win32':
214 214 sec.exclude('plugin.test_exampleip')
215 215 sec.exclude('plugin.dtexample')
216 216
217 217 # terminal:
218 218 if (not have['pexpect']) or (not have['zmq']):
219 219 test_sections['terminal'].exclude('console')
220 220
221 221 # parallel
222 222 sec = test_sections['parallel']
223 223 sec.requires('zmq')
224 224 if not have['pymongo']:
225 225 sec.exclude('controller.mongodb')
226 226 sec.exclude('tests.test_mongodb')
227 227
228 228 # kernel:
229 229 sec = test_sections['kernel']
230 230 sec.requires('zmq')
231 231 # The in-process kernel tests are done in a separate section
232 232 sec.exclude('inprocess')
233 233 # importing gtk sets the default encoding, which we want to avoid
234 234 sec.exclude('zmq.gui.gtkembed')
235 235 sec.exclude('zmq.gui.gtk3embed')
236 236 if not have['matplotlib']:
237 237 sec.exclude('zmq.pylab')
238 238
239 239 # kernel.inprocess:
240 240 test_sections['kernel.inprocess'].requires('zmq')
241 241
242 242 # extensions:
243 243 sec = test_sections['extensions']
244 244 # This is deprecated in favour of rpy2
245 245 sec.exclude('rmagic')
246 246 # autoreload does some strange stuff, so move it to its own test section
247 247 sec.exclude('autoreload')
248 248 sec.exclude('tests.test_autoreload')
249 249 test_sections['autoreload'] = TestSection('autoreload',
250 250 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
251 251 test_group_names.append('autoreload')
252 252
253 253 # qt:
254 254 test_sections['qt'].requires('zmq', 'qt', 'pygments')
255 255
256 256 # html:
257 257 sec = test_sections['html']
258 258 sec.requires('zmq', 'tornado', 'requests', 'sqlite3', 'jsonschema')
259 259 # The notebook 'static' directory contains JS, css and other
260 260 # files for web serving. Occasionally projects may put a .py
261 261 # file in there (MathJax ships a conf.py), so we might as
262 262 # well play it safe and skip the whole thing.
263 263 sec.exclude('static')
264 264 sec.exclude('tasks')
265 265 if not have['jinja2']:
266 266 sec.exclude('notebookapp')
267 267 if not have['pygments'] or not have['jinja2']:
268 268 sec.exclude('nbconvert')
269 269 if not have['terminado']:
270 270 sec.exclude('terminal')
271 271
272 272 # config:
273 273 # Config files aren't really importable stand-alone
274 274 test_sections['config'].exclude('profile')
275 275
276 276 # nbconvert:
277 277 sec = test_sections['nbconvert']
278 278 sec.requires('pygments', 'jinja2', 'jsonschema', 'mistune')
279 279 # Exclude nbconvert directories containing config files used to test.
280 280 # Executing the config files with iptest would cause an exception.
281 281 sec.exclude('tests.files')
282 282 sec.exclude('exporters.tests.files')
283 283 if not have['tornado']:
284 284 sec.exclude('nbconvert.post_processors.serve')
285 285 sec.exclude('nbconvert.post_processors.tests.test_serve')
286 286
287 287 # nbformat:
288 288 test_sections['nbformat'].requires('jsonschema')
289 289
290 290 #-----------------------------------------------------------------------------
291 291 # Functions and classes
292 292 #-----------------------------------------------------------------------------
293 293
294 294 def check_exclusions_exist():
295 295 from IPython.utils.path import get_ipython_package_dir
296 296 from IPython.utils.warn import warn
297 297 parent = os.path.dirname(get_ipython_package_dir())
298 298 for sec in test_sections:
299 299 for pattern in sec.exclusions:
300 300 fullpath = pjoin(parent, pattern)
301 301 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
302 302 warn("Excluding nonexistent file: %r" % pattern)
303 303
304 304
305 305 class ExclusionPlugin(Plugin):
306 306 """A nose plugin to effect our exclusions of files and directories.
307 307 """
308 308 name = 'exclusions'
309 309 score = 3000 # Should come before any other plugins
310 310
311 311 def __init__(self, exclude_patterns=None):
312 312 """
313 313 Parameters
314 314 ----------
315 315
316 316 exclude_patterns : sequence of strings, optional
317 317 Filenames containing these patterns (as raw strings, not as regular
318 318 expressions) are excluded from the tests.
319 319 """
320 320 self.exclude_patterns = exclude_patterns or []
321 321 super(ExclusionPlugin, self).__init__()
322 322
323 323 def options(self, parser, env=os.environ):
324 324 Plugin.options(self, parser, env)
325 325
326 326 def configure(self, options, config):
327 327 Plugin.configure(self, options, config)
328 328 # Override nose trying to disable plugin.
329 329 self.enabled = True
330 330
331 331 def wantFile(self, filename):
332 332 """Return whether the given filename should be scanned for tests.
333 333 """
334 334 if any(pat in filename for pat in self.exclude_patterns):
335 335 return False
336 336 return None
337 337
338 338 def wantDirectory(self, directory):
339 339 """Return whether the given directory should be scanned for tests.
340 340 """
341 341 if any(pat in directory for pat in self.exclude_patterns):
342 342 return False
343 343 return None
344 344
345 345
346 346 class StreamCapturer(Thread):
347 347 daemon = True # Don't hang if main thread crashes
348 348 started = False
349 349 def __init__(self, echo=False):
350 350 super(StreamCapturer, self).__init__()
351 351 self.echo = echo
352 352 self.streams = []
353 353 self.buffer = BytesIO()
354 354 self.readfd, self.writefd = os.pipe()
355 355 self.buffer_lock = Lock()
356 356 self.stop = Event()
357 357
358 358 def run(self):
359 359 self.started = True
360 360
361 361 while not self.stop.is_set():
362 362 chunk = os.read(self.readfd, 1024)
363 363
364 364 with self.buffer_lock:
365 365 self.buffer.write(chunk)
366 366 if self.echo:
367 367 sys.stdout.write(bytes_to_str(chunk))
368 368
369 369 os.close(self.readfd)
370 370 os.close(self.writefd)
371 371
372 372 def reset_buffer(self):
373 373 with self.buffer_lock:
374 374 self.buffer.truncate(0)
375 375 self.buffer.seek(0)
376 376
377 377 def get_buffer(self):
378 378 with self.buffer_lock:
379 379 return self.buffer.getvalue()
380 380
381 381 def ensure_started(self):
382 382 if not self.started:
383 383 self.start()
384 384
385 385 def halt(self):
386 386 """Safely stop the thread."""
387 387 if not self.started:
388 388 return
389 389
390 390 self.stop.set()
391 os.write(self.writefd, b'wake up') # Ensure we're not locked in a read()
391 os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
392 392 self.join()
393 393
394 394 class SubprocessStreamCapturePlugin(Plugin):
395 395 name='subprocstreams'
396 396 def __init__(self):
397 397 Plugin.__init__(self)
398 398 self.stream_capturer = StreamCapturer()
399 399 self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
400 400 # This is ugly, but distant parts of the test machinery need to be able
401 401 # to redirect streams, so we make the object globally accessible.
402 402 nose.iptest_stdstreams_fileno = self.get_write_fileno
403 403
404 404 def get_write_fileno(self):
405 405 if self.destination == 'capture':
406 406 self.stream_capturer.ensure_started()
407 407 return self.stream_capturer.writefd
408 408 elif self.destination == 'discard':
409 409 return os.open(os.devnull, os.O_WRONLY)
410 410 else:
411 411 return sys.__stdout__.fileno()
412 412
413 413 def configure(self, options, config):
414 414 Plugin.configure(self, options, config)
415 415 # Override nose trying to disable plugin.
416 416 if self.destination == 'capture':
417 417 self.enabled = True
418 418
419 419 def startTest(self, test):
420 420 # Reset log capture
421 421 self.stream_capturer.reset_buffer()
422 422
423 423 def formatFailure(self, test, err):
424 424 # Show output
425 425 ec, ev, tb = err
426 426 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
427 427 if captured.strip():
428 428 ev = safe_str(ev)
429 429 out = [ev, '>> begin captured subprocess output <<',
430 430 captured,
431 431 '>> end captured subprocess output <<']
432 432 return ec, '\n'.join(out), tb
433 433
434 434 return err
435 435
436 436 formatError = formatFailure
437 437
438 438 def finalize(self, result):
439 439 self.stream_capturer.halt()
440 440
441 441
442 442 def run_iptest():
443 443 """Run the IPython test suite using nose.
444 444
445 445 This function is called when this script is **not** called with the form
446 446 `iptest all`. It simply calls nose with appropriate command line flags
447 447 and accepts all of the standard nose arguments.
448 448 """
449 449 # Apply our monkeypatch to Xunit
450 450 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
451 451 monkeypatch_xunit()
452 452
453 453 warnings.filterwarnings('ignore',
454 454 'This will be removed soon. Use IPython.testing.util instead')
455 455
456 456 arg1 = sys.argv[1]
457 457 if arg1 in test_sections:
458 458 section = test_sections[arg1]
459 459 sys.argv[1:2] = section.includes
460 460 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
461 461 section = test_sections[arg1[8:]]
462 462 sys.argv[1:2] = section.includes
463 463 else:
464 464 section = TestSection(arg1, includes=[arg1])
465 465
466 466
467 467 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
468 468
469 469 '--with-ipdoctest',
470 470 '--ipdoctest-tests','--ipdoctest-extension=txt',
471 471
472 472 # We add --exe because of setuptools' imbecility (it
473 473 # blindly does chmod +x on ALL files). Nose does the
474 474 # right thing and it tries to avoid executables,
475 475 # setuptools unfortunately forces our hand here. This
476 476 # has been discussed on the distutils list and the
477 477 # setuptools devs refuse to fix this problem!
478 478 '--exe',
479 479 ]
480 480 if '-a' not in argv and '-A' not in argv:
481 481 argv = argv + ['-a', '!crash']
482 482
483 483 if nose.__version__ >= '0.11':
484 484 # I don't fully understand why we need this one, but depending on what
485 485 # directory the test suite is run from, if we don't give it, 0 tests
486 486 # get run. Specifically, if the test suite is run from the source dir
487 487 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
488 488 # even if the same call done in this directory works fine). It appears
489 489 # that if the requested package is in the current dir, nose bails early
490 490 # by default. Since it's otherwise harmless, leave it in by default
491 491 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
492 492 argv.append('--traverse-namespace')
493 493
494 494 # use our plugin for doctesting. It will remove the standard doctest plugin
495 495 # if it finds it enabled
496 496 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
497 497 SubprocessStreamCapturePlugin() ]
498 498
499 499 # Use working directory set by parent process (see iptestcontroller)
500 500 if 'IPTEST_WORKING_DIR' in os.environ:
501 501 os.chdir(os.environ['IPTEST_WORKING_DIR'])
502 502
503 503 # We need a global ipython running in this process, but the special
504 504 # in-process group spawns its own IPython kernels, so for *that* group we
505 505 # must avoid also opening the global one (otherwise there's a conflict of
506 506 # singletons). Ultimately the solution to this problem is to refactor our
507 507 # assumptions about what needs to be a singleton and what doesn't (app
508 508 # objects should, individual shells shouldn't). But for now, this
509 509 # workaround allows the test suite for the inprocess module to complete.
510 510 if 'kernel.inprocess' not in section.name:
511 511 from IPython.testing import globalipapp
512 512 globalipapp.start_ipython()
513 513
514 514 # Now nose can run
515 515 TestProgram(argv=argv, addplugins=plugins)
516 516
517 517 if __name__ == '__main__':
518 518 run_iptest()
519 519
General Comments 0
You need to be logged in to leave comments. Login now