##// END OF EJS Templates
Don't show subprocess output if there isn't any
Thomas Kluyver -
Show More
@@ -1,520 +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 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 from select import select
35 35 import sys
36 36 from threading import Thread, Lock, Event
37 37 import warnings
38 38
39 39 # Now, proceed to import nose itself
40 40 import nose.plugins.builtin
41 41 from nose.plugins.xunit import Xunit
42 42 from nose import SkipTest
43 43 from nose.core import TestProgram
44 44 from nose.plugins import Plugin
45 45 from nose.util import safe_str
46 46
47 47 # Our own imports
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', (2,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['sphinx'] = test_for('sphinx')
153 153
154 154 min_zmq = (2,1,11)
155 155
156 156 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
157 157
158 158 #-----------------------------------------------------------------------------
159 159 # Test suite definitions
160 160 #-----------------------------------------------------------------------------
161 161
162 162 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
163 163 'extensions', 'lib', 'terminal', 'testing', 'utils',
164 164 'nbformat', 'qt', 'html', 'nbconvert'
165 165 ]
166 166
167 167 class TestSection(object):
168 168 def __init__(self, name, includes):
169 169 self.name = name
170 170 self.includes = includes
171 171 self.excludes = []
172 172 self.dependencies = []
173 173 self.enabled = True
174 174
175 175 def exclude(self, module):
176 176 if not module.startswith('IPython'):
177 177 module = self.includes[0] + "." + module
178 178 self.excludes.append(module.replace('.', os.sep))
179 179
180 180 def requires(self, *packages):
181 181 self.dependencies.extend(packages)
182 182
183 183 @property
184 184 def will_run(self):
185 185 return self.enabled and all(have[p] for p in self.dependencies)
186 186
187 187 # Name -> (include, exclude, dependencies_met)
188 188 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
189 189
190 190 # Exclusions and dependencies
191 191 # ---------------------------
192 192
193 193 # core:
194 194 sec = test_sections['core']
195 195 if not have['sqlite3']:
196 196 sec.exclude('tests.test_history')
197 197 sec.exclude('history')
198 198 if not have['matplotlib']:
199 199 sec.exclude('pylabtools'),
200 200 sec.exclude('tests.test_pylabtools')
201 201
202 202 # lib:
203 203 sec = test_sections['lib']
204 204 if not have['wx']:
205 205 sec.exclude('inputhookwx')
206 206 if not have['pexpect']:
207 207 sec.exclude('irunner')
208 208 sec.exclude('tests.test_irunner')
209 209 if not have['zmq']:
210 210 sec.exclude('kernel')
211 211 # We do this unconditionally, so that the test suite doesn't import
212 212 # gtk, changing the default encoding and masking some unicode bugs.
213 213 sec.exclude('inputhookgtk')
214 214 # Testing inputhook will need a lot of thought, to figure out
215 215 # how to have tests that don't lock up with the gui event
216 216 # loops in the picture
217 217 sec.exclude('inputhook')
218 218
219 219 # testing:
220 220 sec = test_sections['testing']
221 221 # This guy is probably attic material
222 222 sec.exclude('mkdoctests')
223 223 # These have to be skipped on win32 because they use echo, rm, cd, etc.
224 224 # See ticket https://github.com/ipython/ipython/issues/87
225 225 if sys.platform == 'win32':
226 226 sec.exclude('plugin.test_exampleip')
227 227 sec.exclude('plugin.dtexample')
228 228
229 229 # terminal:
230 230 if (not have['pexpect']) or (not have['zmq']):
231 231 test_sections['terminal'].exclude('console')
232 232
233 233 # parallel
234 234 sec = test_sections['parallel']
235 235 sec.requires('zmq')
236 236 if not have['pymongo']:
237 237 sec.exclude('controller.mongodb')
238 238 sec.exclude('tests.test_mongodb')
239 239
240 240 # kernel:
241 241 sec = test_sections['kernel']
242 242 sec.requires('zmq')
243 243 # The in-process kernel tests are done in a separate section
244 244 sec.exclude('inprocess')
245 245 # importing gtk sets the default encoding, which we want to avoid
246 246 sec.exclude('zmq.gui.gtkembed')
247 247 if not have['matplotlib']:
248 248 sec.exclude('zmq.pylab')
249 249
250 250 # kernel.inprocess:
251 251 test_sections['kernel.inprocess'].requires('zmq')
252 252
253 253 # extensions:
254 254 sec = test_sections['extensions']
255 255 if not have['cython']:
256 256 sec.exclude('cythonmagic')
257 257 sec.exclude('tests.test_cythonmagic')
258 258 if not have['oct2py']:
259 259 sec.exclude('octavemagic')
260 260 sec.exclude('tests.test_octavemagic')
261 261 if not have['rpy2'] or not have['numpy']:
262 262 sec.exclude('rmagic')
263 263 sec.exclude('tests.test_rmagic')
264 264 # autoreload does some strange stuff, so move it to its own test section
265 265 sec.exclude('autoreload')
266 266 sec.exclude('tests.test_autoreload')
267 267 test_sections['autoreload'] = TestSection('autoreload',
268 268 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
269 269 test_group_names.append('autoreload')
270 270
271 271 # qt:
272 272 test_sections['qt'].requires('zmq', 'qt', 'pygments')
273 273
274 274 # html:
275 275 sec = test_sections['html']
276 276 sec.requires('zmq', 'tornado')
277 277 # The notebook 'static' directory contains JS, css and other
278 278 # files for web serving. Occasionally projects may put a .py
279 279 # file in there (MathJax ships a conf.py), so we might as
280 280 # well play it safe and skip the whole thing.
281 281 sec.exclude('static')
282 282 sec.exclude('fabfile')
283 283 if not have['jinja2']:
284 284 sec.exclude('notebookapp')
285 285 if not have['azure']:
286 286 sec.exclude('services.notebooks.azurenbmanager')
287 287
288 288 # config:
289 289 # Config files aren't really importable stand-alone
290 290 test_sections['config'].exclude('profile')
291 291
292 292 # nbconvert:
293 293 sec = test_sections['nbconvert']
294 294 sec.requires('pygments', 'jinja2', 'sphinx')
295 295 # Exclude nbconvert directories containing config files used to test.
296 296 # Executing the config files with iptest would cause an exception.
297 297 sec.exclude('tests.files')
298 298 sec.exclude('exporters.tests.files')
299 299 if not have['tornado']:
300 300 sec.exclude('nbconvert.post_processors.serve')
301 301 sec.exclude('nbconvert.post_processors.tests.test_serve')
302 302
303 303 #-----------------------------------------------------------------------------
304 304 # Functions and classes
305 305 #-----------------------------------------------------------------------------
306 306
307 307 def check_exclusions_exist():
308 308 from IPython.utils.path import get_ipython_package_dir
309 309 from IPython.utils.warn import warn
310 310 parent = os.path.dirname(get_ipython_package_dir())
311 311 for sec in test_sections:
312 312 for pattern in sec.exclusions:
313 313 fullpath = pjoin(parent, pattern)
314 314 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
315 315 warn("Excluding nonexistent file: %r" % pattern)
316 316
317 317
318 318 class ExclusionPlugin(Plugin):
319 319 """A nose plugin to effect our exclusions of files and directories.
320 320 """
321 321 name = 'exclusions'
322 322 score = 3000 # Should come before any other plugins
323 323
324 324 def __init__(self, exclude_patterns=None):
325 325 """
326 326 Parameters
327 327 ----------
328 328
329 329 exclude_patterns : sequence of strings, optional
330 330 Filenames containing these patterns (as raw strings, not as regular
331 331 expressions) are excluded from the tests.
332 332 """
333 333 self.exclude_patterns = exclude_patterns or []
334 334 super(ExclusionPlugin, self).__init__()
335 335
336 336 def options(self, parser, env=os.environ):
337 337 Plugin.options(self, parser, env)
338 338
339 339 def configure(self, options, config):
340 340 Plugin.configure(self, options, config)
341 341 # Override nose trying to disable plugin.
342 342 self.enabled = True
343 343
344 344 def wantFile(self, filename):
345 345 """Return whether the given filename should be scanned for tests.
346 346 """
347 347 if any(pat in filename for pat in self.exclude_patterns):
348 348 return False
349 349 return None
350 350
351 351 def wantDirectory(self, directory):
352 352 """Return whether the given directory should be scanned for tests.
353 353 """
354 354 if any(pat in directory for pat in self.exclude_patterns):
355 355 return False
356 356 return None
357 357
358 358
359 359 class StreamCapturer(Thread):
360 360 started = False
361 361 def __init__(self):
362 362 super(StreamCapturer, self).__init__()
363 363 self.streams = []
364 364 self.buffer = BytesIO()
365 365 self.streams_lock = Lock()
366 366 self.buffer_lock = Lock()
367 367 self.stream_added = Event()
368 368 self.stop = Event()
369 369
370 370 def run(self):
371 371 self.started = True
372 372 while not self.stop.is_set():
373 373 with self.streams_lock:
374 374 streams = self.streams
375 375
376 376 if not streams:
377 377 self.stream_added.wait(timeout=1)
378 378 self.stream_added.clear()
379 379 continue
380 380
381 381 ready = select(streams, [], [], 0.5)[0]
382 382 with self.buffer_lock:
383 383 for fd in ready:
384 384 self.buffer.write(os.read(fd, 1024))
385 385
386 386 def add_stream(self, fd):
387 387 with self.streams_lock:
388 388 self.streams.append(fd)
389 389 self.stream_added.set()
390 390
391 391 def remove_stream(self, fd):
392 392 with self.streams_lock:
393 393 self.streams.remove(fd)
394 394
395 395 def reset_buffer(self):
396 396 with self.buffer_lock:
397 397 self.buffer.truncate(0)
398 398 self.buffer.seek(0)
399 399
400 400 def get_buffer(self):
401 401 with self.buffer_lock:
402 402 return self.buffer.getvalue()
403 403
404 404 def ensure_started(self):
405 405 if not self.started:
406 406 self.start()
407 407
408 408 class SubprocessStreamCapturePlugin(Plugin):
409 409 name='subprocstreams'
410 410 def __init__(self):
411 411 Plugin.__init__(self)
412 412 self.stream_capturer = StreamCapturer()
413 413 # This is ugly, but distant parts of the test machinery need to be able
414 414 # to add streams, so we make the object globally accessible.
415 415 nose.ipy_stream_capturer = self.stream_capturer
416 416
417 417 def configure(self, options, config):
418 418 Plugin.configure(self, options, config)
419 419 # Override nose trying to disable plugin.
420 420 self.enabled = True
421 421
422 422 def startTest(self, test):
423 423 # Reset log capture
424 424 self.stream_capturer.reset_buffer()
425 425
426 426 def formatFailure(self, test, err):
427 427 # Show output
428 428 ec, ev, tb = err
429 ev = safe_str(ev)
430 out = [ev, '>> begin captured subprocess output <<',
431 self.stream_capturer.get_buffer().decode('utf-8', 'replace'),
432 '>> end captured subprocess output <<']
433 return ec, '\n'.join(out), tb
429 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
430 if captured.strip():
431 ev = safe_str(ev)
432 out = [ev, '>> begin captured subprocess output <<',
433 captured,
434 '>> end captured subprocess output <<']
435 return ec, '\n'.join(out), tb
436
437 return err
434 438
435 439 formatError = formatFailure
436 440
437 441 def finalize(self, result):
438 442 if self.stream_capturer.started:
439 443 self.stream_capturer.stop.set()
440 444 self.stream_capturer.join()
441 445
442 446
443 447 def run_iptest():
444 448 """Run the IPython test suite using nose.
445 449
446 450 This function is called when this script is **not** called with the form
447 451 `iptest all`. It simply calls nose with appropriate command line flags
448 452 and accepts all of the standard nose arguments.
449 453 """
450 454 # Apply our monkeypatch to Xunit
451 455 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
452 456 monkeypatch_xunit()
453 457
454 458 warnings.filterwarnings('ignore',
455 459 'This will be removed soon. Use IPython.testing.util instead')
456 460
457 461 arg1 = sys.argv[1]
458 462 if arg1 in test_sections:
459 463 section = test_sections[arg1]
460 464 sys.argv[1:2] = section.includes
461 465 elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
462 466 section = test_sections[arg1[8:]]
463 467 sys.argv[1:2] = section.includes
464 468 else:
465 469 section = TestSection(arg1, includes=[arg1])
466 470
467 471
468 472 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
469 473
470 474 '--with-ipdoctest',
471 475 '--ipdoctest-tests','--ipdoctest-extension=txt',
472 476
473 477 # We add --exe because of setuptools' imbecility (it
474 478 # blindly does chmod +x on ALL files). Nose does the
475 479 # right thing and it tries to avoid executables,
476 480 # setuptools unfortunately forces our hand here. This
477 481 # has been discussed on the distutils list and the
478 482 # setuptools devs refuse to fix this problem!
479 483 '--exe',
480 484 ]
481 485 if '-a' not in argv and '-A' not in argv:
482 486 argv = argv + ['-a', '!crash']
483 487
484 488 if nose.__version__ >= '0.11':
485 489 # I don't fully understand why we need this one, but depending on what
486 490 # directory the test suite is run from, if we don't give it, 0 tests
487 491 # get run. Specifically, if the test suite is run from the source dir
488 492 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
489 493 # even if the same call done in this directory works fine). It appears
490 494 # that if the requested package is in the current dir, nose bails early
491 495 # by default. Since it's otherwise harmless, leave it in by default
492 496 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
493 497 argv.append('--traverse-namespace')
494 498
495 499 # use our plugin for doctesting. It will remove the standard doctest plugin
496 500 # if it finds it enabled
497 501 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure(),
498 502 SubprocessStreamCapturePlugin() ]
499 503
500 504 # Use working directory set by parent process (see iptestcontroller)
501 505 if 'IPTEST_WORKING_DIR' in os.environ:
502 506 os.chdir(os.environ['IPTEST_WORKING_DIR'])
503 507
504 508 # We need a global ipython running in this process, but the special
505 509 # in-process group spawns its own IPython kernels, so for *that* group we
506 510 # must avoid also opening the global one (otherwise there's a conflict of
507 511 # singletons). Ultimately the solution to this problem is to refactor our
508 512 # assumptions about what needs to be a singleton and what doesn't (app
509 513 # objects should, individual shells shouldn't). But for now, this
510 514 # workaround allows the test suite for the inprocess module to complete.
511 515 if 'kernel.inprocess' not in section.name:
512 516 from IPython.testing import globalipapp
513 517 globalipapp.start_ipython()
514 518
515 519 # Now nose can run
516 520 TestProgram(argv=argv, addplugins=plugins)
517 521
518 522 if __name__ == '__main__':
519 523 run_iptest()
520 524
General Comments 0
You need to be logged in to leave comments. Login now