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