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