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