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