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