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