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