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