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