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