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