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