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