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