##// END OF EJS Templates
Start refactoring test machinery
Thomas Kluyver -
Show More
@@ -1,365 +1,426 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Suite Runner.
2 """IPython Test Suite Runner.
3
3
4 This module provides a main entry point to a user script to test IPython
4 This module provides a main entry point to a user script to test IPython
5 itself from the command line. There are two ways of running this script:
5 itself from the command line. There are two ways of running this script:
6
6
7 1. With the syntax `iptest all`. This runs our entire test suite by
7 1. With the syntax `iptest all`. This runs our entire test suite by
8 calling this script (with different arguments) recursively. This
8 calling this script (with different arguments) recursively. This
9 causes modules and package to be tested in different processes, using nose
9 causes modules and package to be tested in different processes, using nose
10 or trial where appropriate.
10 or trial where appropriate.
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 the script simply calls nose, but with special command line flags and
12 the script simply calls nose, but with special command line flags and
13 plugins loaded.
13 plugins loaded.
14
14
15 """
15 """
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Copyright (C) 2009-2011 The IPython Development Team
18 # Copyright (C) 2009-2011 The IPython Development Team
19 #
19 #
20 # Distributed under the terms of the BSD License. The full license is in
20 # Distributed under the terms of the BSD License. The full license is in
21 # the file COPYING, distributed as part of this software.
21 # the file COPYING, distributed as part of this software.
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Imports
25 # Imports
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 from __future__ import print_function
27 from __future__ import print_function
28
28
29 # Stdlib
29 # Stdlib
30 import glob
30 import glob
31 import os
31 import os
32 import os.path as path
32 import os.path as path
33 import re
33 import sys
34 import sys
34 import warnings
35 import warnings
35
36
36 # Now, proceed to import nose itself
37 # Now, proceed to import nose itself
37 import nose.plugins.builtin
38 import nose.plugins.builtin
38 from nose.plugins.xunit import Xunit
39 from nose.plugins.xunit import Xunit
39 from nose import SkipTest
40 from nose import SkipTest
40 from nose.core import TestProgram
41 from nose.core import TestProgram
42 from nose.plugins import Plugin
41
43
42 # Our own imports
44 # Our own imports
43 from IPython.utils.importstring import import_item
45 from IPython.utils.importstring import import_item
44 from IPython.utils.path import get_ipython_package_dir
46 from IPython.utils.path import get_ipython_package_dir
45 from IPython.utils.warn import warn
47 from IPython.utils.warn import warn
46
48
47 from IPython.testing import globalipapp
49 from IPython.testing import globalipapp
48 from IPython.testing.plugin.ipdoctest import IPythonDoctest
50 from IPython.testing.plugin.ipdoctest import IPythonDoctest
49 from IPython.external.decorators import KnownFailure, knownfailureif
51 from IPython.external.decorators import KnownFailure, knownfailureif
50
52
51 pjoin = path.join
53 pjoin = path.join
52
54
53
55
54 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
55 # Globals
57 # Globals
56 #-----------------------------------------------------------------------------
58 #-----------------------------------------------------------------------------
57
59
58
60
59 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
60 # Warnings control
62 # Warnings control
61 #-----------------------------------------------------------------------------
63 #-----------------------------------------------------------------------------
62
64
63 # Twisted generates annoying warnings with Python 2.6, as will do other code
65 # Twisted generates annoying warnings with Python 2.6, as will do other code
64 # that imports 'sets' as of today
66 # that imports 'sets' as of today
65 warnings.filterwarnings('ignore', 'the sets module is deprecated',
67 warnings.filterwarnings('ignore', 'the sets module is deprecated',
66 DeprecationWarning )
68 DeprecationWarning )
67
69
68 # This one also comes from Twisted
70 # This one also comes from Twisted
69 warnings.filterwarnings('ignore', 'the sha module is deprecated',
71 warnings.filterwarnings('ignore', 'the sha module is deprecated',
70 DeprecationWarning)
72 DeprecationWarning)
71
73
72 # Wx on Fedora11 spits these out
74 # Wx on Fedora11 spits these out
73 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
75 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
74 UserWarning)
76 UserWarning)
75
77
76 # ------------------------------------------------------------------------------
78 # ------------------------------------------------------------------------------
77 # Monkeypatch Xunit to count known failures as skipped.
79 # Monkeypatch Xunit to count known failures as skipped.
78 # ------------------------------------------------------------------------------
80 # ------------------------------------------------------------------------------
79 def monkeypatch_xunit():
81 def monkeypatch_xunit():
80 try:
82 try:
81 knownfailureif(True)(lambda: None)()
83 knownfailureif(True)(lambda: None)()
82 except Exception as e:
84 except Exception as e:
83 KnownFailureTest = type(e)
85 KnownFailureTest = type(e)
84
86
85 def addError(self, test, err, capt=None):
87 def addError(self, test, err, capt=None):
86 if issubclass(err[0], KnownFailureTest):
88 if issubclass(err[0], KnownFailureTest):
87 err = (SkipTest,) + err[1:]
89 err = (SkipTest,) + err[1:]
88 return self.orig_addError(test, err, capt)
90 return self.orig_addError(test, err, capt)
89
91
90 Xunit.orig_addError = Xunit.addError
92 Xunit.orig_addError = Xunit.addError
91 Xunit.addError = addError
93 Xunit.addError = addError
92
94
93 #-----------------------------------------------------------------------------
95 #-----------------------------------------------------------------------------
94 # Logic for skipping doctests
96 # Check which dependencies are installed and greater than minimum version.
95 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
96 def extract_version(mod):
98 def extract_version(mod):
97 return mod.__version__
99 return mod.__version__
98
100
99 def test_for(item, min_version=None, callback=extract_version):
101 def test_for(item, min_version=None, callback=extract_version):
100 """Test to see if item is importable, and optionally check against a minimum
102 """Test to see if item is importable, and optionally check against a minimum
101 version.
103 version.
102
104
103 If min_version is given, the default behavior is to check against the
105 If min_version is given, the default behavior is to check against the
104 `__version__` attribute of the item, but specifying `callback` allows you to
106 `__version__` attribute of the item, but specifying `callback` allows you to
105 extract the value you are interested in. e.g::
107 extract the value you are interested in. e.g::
106
108
107 In [1]: import sys
109 In [1]: import sys
108
110
109 In [2]: from IPython.testing.iptest import test_for
111 In [2]: from IPython.testing.iptest import test_for
110
112
111 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
113 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
112 Out[3]: True
114 Out[3]: True
113
115
114 """
116 """
115 try:
117 try:
116 check = import_item(item)
118 check = import_item(item)
117 except (ImportError, RuntimeError):
119 except (ImportError, RuntimeError):
118 # GTK reports Runtime error if it can't be initialized even if it's
120 # GTK reports Runtime error if it can't be initialized even if it's
119 # importable.
121 # importable.
120 return False
122 return False
121 else:
123 else:
122 if min_version:
124 if min_version:
123 if callback:
125 if callback:
124 # extra processing step to get version to compare
126 # extra processing step to get version to compare
125 check = callback(check)
127 check = callback(check)
126
128
127 return check >= min_version
129 return check >= min_version
128 else:
130 else:
129 return True
131 return True
130
132
131 # Global dict where we can store information on what we have and what we don't
133 # Global dict where we can store information on what we have and what we don't
132 # have available at test run time
134 # have available at test run time
133 have = {}
135 have = {}
134
136
135 have['curses'] = test_for('_curses')
137 have['curses'] = test_for('_curses')
136 have['matplotlib'] = test_for('matplotlib')
138 have['matplotlib'] = test_for('matplotlib')
137 have['numpy'] = test_for('numpy')
139 have['numpy'] = test_for('numpy')
138 have['pexpect'] = test_for('IPython.external.pexpect')
140 have['pexpect'] = test_for('IPython.external.pexpect')
139 have['pymongo'] = test_for('pymongo')
141 have['pymongo'] = test_for('pymongo')
140 have['pygments'] = test_for('pygments')
142 have['pygments'] = test_for('pygments')
141 have['qt'] = test_for('IPython.external.qt')
143 have['qt'] = test_for('IPython.external.qt')
142 have['rpy2'] = test_for('rpy2')
144 have['rpy2'] = test_for('rpy2')
143 have['sqlite3'] = test_for('sqlite3')
145 have['sqlite3'] = test_for('sqlite3')
144 have['cython'] = test_for('Cython')
146 have['cython'] = test_for('Cython')
145 have['oct2py'] = test_for('oct2py')
147 have['oct2py'] = test_for('oct2py')
146 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
148 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
147 have['jinja2'] = test_for('jinja2')
149 have['jinja2'] = test_for('jinja2')
148 have['wx'] = test_for('wx')
150 have['wx'] = test_for('wx')
149 have['wx.aui'] = test_for('wx.aui')
151 have['wx.aui'] = test_for('wx.aui')
150 have['azure'] = test_for('azure')
152 have['azure'] = test_for('azure')
151 have['sphinx'] = test_for('sphinx')
153 have['sphinx'] = test_for('sphinx')
152
154
153 min_zmq = (2,1,11)
155 min_zmq = (2,1,11)
154
156
155 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
157 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
156
158
157 #-----------------------------------------------------------------------------
159 #-----------------------------------------------------------------------------
158 # Functions and classes
160 # Test suite definitions
159 #-----------------------------------------------------------------------------
161 #-----------------------------------------------------------------------------
160
162
163 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
164 'extensions', 'lib', 'terminal', 'testing', 'utils',
165 'nbformat', 'qt', 'html', 'nbconvert'
166 ]
167
168 class TestSection(object):
169 def __init__(self, name, includes):
170 self.name = name
171 self.includes = includes
172 self.excludes = []
173 self.dependencies = []
174 self.enabled = True
175
176 def exclude(self, module):
177 if not module.startswith('IPython'):
178 module = self.includes[0] + "." + module
179 self.excludes.append(module.replace('.', os.sep))
180
181 def requires(self, *packages):
182 self.dependencies.extend(packages)
183
184 @property
185 def will_run(self):
186 return self.enabled and all(have[p] for p in self.dependencies)
187
188 # Name -> (include, exclude, dependencies_met)
189 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
190
191 # Exclusions and dependencies
192 # ---------------------------
193
194 # core:
195 sec = test_sections['core']
196 if not have['sqlite3']:
197 sec.exclude('tests.test_history')
198 sec.exclude('history')
199 if not have['matplotlib']:
200 sec.exclude('pylabtools'),
201 sec.exclude('tests.test_pylabtools')
202
203 # lib:
204 sec = test_sections['lib']
205 if not have['wx']:
206 sec.exclude('inputhookwx')
207 if not have['pexpect']:
208 sec.exclude('irunner')
209 sec.exclude('tests.test_irunner')
210 if not have['zmq']:
211 sec.exclude('kernel')
212 # We do this unconditionally, so that the test suite doesn't import
213 # gtk, changing the default encoding and masking some unicode bugs.
214 sec.exclude('inputhookgtk')
215 # Testing inputhook will need a lot of thought, to figure out
216 # how to have tests that don't lock up with the gui event
217 # loops in the picture
218 sec.exclude('inputhook')
219
220 # testing:
221 sec = test_sections['lib']
222 # This guy is probably attic material
223 sec.exclude('mkdoctests')
224 # These have to be skipped on win32 because the use echo, rm, cd, etc.
225 # See ticket https://github.com/ipython/ipython/issues/87
226 if sys.platform == 'win32':
227 sec.exclude('plugin.test_exampleip')
228 sec.exclude('plugin.dtexample')
229
230 # terminal:
231 if (not have['pexpect']) or (not have['zmq']):
232 test_sections['terminal'].exclude('console')
233
234 # parallel
235 sec = test_sections['parallel']
236 sec.requires('zmq')
237 if not have['pymongo']:
238 sec.exclude('controller.mongodb')
239 sec.exclude('tests.test_mongodb')
240
241 # kernel:
242 sec = test_sections['kernel']
243 sec.requires('zmq')
244 # The in-process kernel tests are done in a separate section
245 sec.exclude('inprocess')
246 # importing gtk sets the default encoding, which we want to avoid
247 sec.exclude('zmq.gui.gtkembed')
248 if not have['matplotlib']:
249 sec.exclude('zmq.pylab')
250
251 # kernel.inprocess:
252 test_sections['kernel.inprocess'].requires('zmq')
253
254 # extensions:
255 sec = test_sections['extensions']
256 if not have['cython']:
257 sec.exclude('cythonmagic')
258 sec.exclude('tests.test_cythonmagic')
259 if not have['oct2py']:
260 sec.exclude('octavemagic')
261 sec.exclude('tests.test_octavemagic')
262 if not have['rpy2'] or not have['numpy']:
263 sec.exclude('rmagic')
264 sec.exclude('tests.test_rmagic')
265 # autoreload does some strange stuff, so move it to its own test section
266 sec.exclude('autoreload')
267 sec.exclude('tests.test_autoreload')
268 test_sections['autoreload'] = TestSection('autoreload',
269 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
270 test_group_names.append('autoreload')
271
272 # qt:
273 test_sections['qt'].requires('zmq', 'qt', 'pygments')
274
275 # html:
276 sec = test_sections['html']
277 sec.requires('zmq', 'tornado')
278 # The notebook 'static' directory contains JS, css and other
279 # files for web serving. Occasionally projects may put a .py
280 # file in there (MathJax ships a conf.py), so we might as
281 # well play it safe and skip the whole thing.
282 sec.exclude('static')
283 sec.exclude('fabfile')
284 if not have['jinja2']:
285 sec.exclude('notebookapp')
286 if not have['azure']:
287 sec.exclude('services.notebooks.azurenbmanager')
288
289 # config:
290 # Config files aren't really importable stand-alone
291 test_sections['config'].exclude('profile')
292
293 # nbconvert:
294 sec = test_sections['nbconvert']
295 sec.requires('pygments', 'jinja2', 'sphinx')
296 # Exclude nbconvert directories containing config files used to test.
297 # Executing the config files with iptest would cause an exception.
298 sec.exclude('tests.files')
299 sec.exclude('exporters.tests.files')
161
300
162 def make_exclude():
301 #-----------------------------------------------------------------------------
163 """Make patterns of modules and packages to exclude from testing.
302 # Functions and classes
303 #-----------------------------------------------------------------------------
164
304
165 For the IPythonDoctest plugin, we need to exclude certain patterns that
305 def check_exclusions_exist():
166 cause testing problems. We should strive to minimize the number of
306 parent = os.path.dirname(get_ipython_package_dir())
167 skipped modules, since this means untested code.
307 for sec in test_sections:
308 for pattern in sec.exclusions:
309 fullpath = pjoin(parent, pattern)
310 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
311 warn("Excluding nonexistent file: %r" % pattern)
168
312
169 These modules and packages will NOT get scanned by nose at all for tests.
313
314 class ExclusionPlugin(Plugin):
315 """A nose plugin to effect our exclusions of files and directories.
170 """
316 """
171 # Simple utility to make IPython paths more readably, we need a lot of
317 name = 'exclusions'
172 # these below
318 score = 3000 # Should come before any other plugins
173 ipjoin = lambda *paths: pjoin('IPython', *paths)
174
175 exclusions = [ipjoin('external'),
176 ipjoin('quarantine'),
177 ipjoin('deathrow'),
178 # This guy is probably attic material
179 ipjoin('testing', 'mkdoctests'),
180 # Testing inputhook will need a lot of thought, to figure out
181 # how to have tests that don't lock up with the gui event
182 # loops in the picture
183 ipjoin('lib', 'inputhook'),
184 # Config files aren't really importable stand-alone
185 ipjoin('config', 'profile'),
186 # The notebook 'static' directory contains JS, css and other
187 # files for web serving. Occasionally projects may put a .py
188 # file in there (MathJax ships a conf.py), so we might as
189 # well play it safe and skip the whole thing.
190 ipjoin('html', 'static'),
191 ipjoin('html', 'fabfile'),
192 ]
193 if not have['sqlite3']:
194 exclusions.append(ipjoin('core', 'tests', 'test_history'))
195 exclusions.append(ipjoin('core', 'history'))
196 if not have['wx']:
197 exclusions.append(ipjoin('lib', 'inputhookwx'))
198
199 if 'IPython.kernel.inprocess' not in sys.argv:
200 exclusions.append(ipjoin('kernel', 'inprocess'))
201
319
202 # FIXME: temporarily disable autoreload tests, as they can produce
320 def __init__(self, exclude_patterns=None):
203 # spurious failures in subsequent tests (cythonmagic).
321 """
204 exclusions.append(ipjoin('extensions', 'autoreload'))
322 Parameters
205 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
323 ----------
206
324
207 # We do this unconditionally, so that the test suite doesn't import
325 exclude_patterns : sequence of strings, optional
208 # gtk, changing the default encoding and masking some unicode bugs.
326 These patterns are compiled as regular expressions, subsequently used
209 exclusions.append(ipjoin('lib', 'inputhookgtk'))
327 to exclude any filename which matches them from inclusion in the test
210 exclusions.append(ipjoin('kernel', 'zmq', 'gui', 'gtkembed'))
328 suite (using pattern.search(), NOT pattern.match() ).
211
329 """
212 #Also done unconditionally, exclude nbconvert directories containing
330
213 #config files used to test. Executing the config files with iptest would
331 if exclude_patterns is None:
214 #cause an exception.
332 exclude_patterns = []
215 exclusions.append(ipjoin('nbconvert', 'tests', 'files'))
333 self.exclude_patterns = [re.compile(p) for p in exclude_patterns]
216 exclusions.append(ipjoin('nbconvert', 'exporters', 'tests', 'files'))
334 super(ExclusionPlugin, self).__init__()
217
335
218 # These have to be skipped on win32 because the use echo, rm, cd, etc.
336 def options(self, parser, env=os.environ):
219 # See ticket https://github.com/ipython/ipython/issues/87
337 Plugin.options(self, parser, env)
220 if sys.platform == 'win32':
221 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
222 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
223
224 if not have['pexpect']:
225 exclusions.extend([ipjoin('lib', 'irunner'),
226 ipjoin('lib', 'tests', 'test_irunner'),
227 ipjoin('terminal', 'console'),
228 ])
229
230 if not have['zmq']:
231 exclusions.append(ipjoin('lib', 'kernel'))
232 exclusions.append(ipjoin('kernel'))
233 exclusions.append(ipjoin('qt'))
234 exclusions.append(ipjoin('html'))
235 exclusions.append(ipjoin('consoleapp.py'))
236 exclusions.append(ipjoin('terminal', 'console'))
237 exclusions.append(ipjoin('parallel'))
238 elif not have['qt'] or not have['pygments']:
239 exclusions.append(ipjoin('qt'))
240
241 if not have['pymongo']:
242 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
243 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
244
245 if not have['matplotlib']:
246 exclusions.extend([ipjoin('core', 'pylabtools'),
247 ipjoin('core', 'tests', 'test_pylabtools'),
248 ipjoin('kernel', 'zmq', 'pylab'),
249 ])
250
251 if not have['cython']:
252 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
253 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
254
255 if not have['oct2py']:
256 exclusions.extend([ipjoin('extensions', 'octavemagic')])
257 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
258
259 if not have['tornado']:
260 exclusions.append(ipjoin('html'))
261 exclusions.append(ipjoin('nbconvert', 'post_processors', 'serve'))
262 exclusions.append(ipjoin('nbconvert', 'post_processors', 'tests', 'test_serve'))
263
264 if not have['jinja2']:
265 exclusions.append(ipjoin('html', 'notebookapp'))
266
267 if not have['rpy2'] or not have['numpy']:
268 exclusions.append(ipjoin('extensions', 'rmagic'))
269 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
270
271 if not have['azure']:
272 exclusions.append(ipjoin('html', 'services', 'notebooks', 'azurenbmanager'))
273
274 if not all((have['pygments'], have['jinja2'], have['sphinx'])):
275 exclusions.append(ipjoin('nbconvert'))
276
277 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
278 if sys.platform == 'win32':
279 exclusions = [s.replace('\\','\\\\') for s in exclusions]
280
338
281 # check for any exclusions that don't seem to exist:
339 def configure(self, options, config):
282 parent, _ = os.path.split(get_ipython_package_dir())
340 Plugin.configure(self, options, config)
283 for exclusion in exclusions:
341 # Override nose trying to disable plugin.
284 if exclusion.endswith(('deathrow', 'quarantine')):
342 self.enabled = True
285 # ignore deathrow/quarantine, which exist in dev, but not install
343
286 continue
344 def wantFile(self, filename):
287 fullpath = pjoin(parent, exclusion)
345 """Return whether the given filename should be scanned for tests.
288 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
346 """
289 warn("Excluding nonexistent file: %r" % exclusion)
347 if any(pat.search(filename) for pat in self.exclude_patterns):
290
348 return False
291 return exclusions
349 return None
292
350
293 special_test_suites = {
351 def wantDirectory(self, directory):
294 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
352 """Return whether the given directory should be scanned for tests.
295 }
353 """
354 if any(pat.search(directory) for pat in self.exclude_patterns):
355 return False
356 return None
296
357
297
358
298 def run_iptest():
359 def run_iptest():
299 """Run the IPython test suite using nose.
360 """Run the IPython test suite using nose.
300
361
301 This function is called when this script is **not** called with the form
362 This function is called when this script is **not** called with the form
302 `iptest all`. It simply calls nose with appropriate command line flags
363 `iptest all`. It simply calls nose with appropriate command line flags
303 and accepts all of the standard nose arguments.
364 and accepts all of the standard nose arguments.
304 """
365 """
305 # Apply our monkeypatch to Xunit
366 # Apply our monkeypatch to Xunit
306 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
367 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
307 monkeypatch_xunit()
368 monkeypatch_xunit()
308
369
309 warnings.filterwarnings('ignore',
370 warnings.filterwarnings('ignore',
310 'This will be removed soon. Use IPython.testing.util instead')
371 'This will be removed soon. Use IPython.testing.util instead')
311
372
312 if sys.argv[1] in special_test_suites:
373 section = test_sections[sys.argv[1]]
313 sys.argv[1:2] = special_test_suites[sys.argv[1]]
374 sys.argv[1:2] = section.includes
314 special_suite = True
315 else:
316 special_suite = False
317
375
318 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
376 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
319
377
320 '--with-ipdoctest',
378 '--with-ipdoctest',
321 '--ipdoctest-tests','--ipdoctest-extension=txt',
379 '--ipdoctest-tests','--ipdoctest-extension=txt',
322
380
323 # We add --exe because of setuptools' imbecility (it
381 # We add --exe because of setuptools' imbecility (it
324 # blindly does chmod +x on ALL files). Nose does the
382 # blindly does chmod +x on ALL files). Nose does the
325 # right thing and it tries to avoid executables,
383 # right thing and it tries to avoid executables,
326 # setuptools unfortunately forces our hand here. This
384 # setuptools unfortunately forces our hand here. This
327 # has been discussed on the distutils list and the
385 # has been discussed on the distutils list and the
328 # setuptools devs refuse to fix this problem!
386 # setuptools devs refuse to fix this problem!
329 '--exe',
387 '--exe',
330 ]
388 ]
331 if '-a' not in argv and '-A' not in argv:
389 if '-a' not in argv and '-A' not in argv:
332 argv = argv + ['-a', '!crash']
390 argv = argv + ['-a', '!crash']
333
391
334 if nose.__version__ >= '0.11':
392 if nose.__version__ >= '0.11':
335 # I don't fully understand why we need this one, but depending on what
393 # I don't fully understand why we need this one, but depending on what
336 # directory the test suite is run from, if we don't give it, 0 tests
394 # directory the test suite is run from, if we don't give it, 0 tests
337 # get run. Specifically, if the test suite is run from the source dir
395 # get run. Specifically, if the test suite is run from the source dir
338 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
396 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
339 # even if the same call done in this directory works fine). It appears
397 # even if the same call done in this directory works fine). It appears
340 # that if the requested package is in the current dir, nose bails early
398 # that if the requested package is in the current dir, nose bails early
341 # by default. Since it's otherwise harmless, leave it in by default
399 # by default. Since it's otherwise harmless, leave it in by default
342 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
400 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
343 argv.append('--traverse-namespace')
401 argv.append('--traverse-namespace')
344
402
345 # use our plugin for doctesting. It will remove the standard doctest plugin
403 # use our plugin for doctesting. It will remove the standard doctest plugin
346 # if it finds it enabled
404 # if it finds it enabled
347 ipdt = IPythonDoctest() if special_suite else IPythonDoctest(make_exclude())
405 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure()]
348 plugins = [ipdt, KnownFailure()]
406
407 # Use working directory set by parent process (see iptestcontroller)
408 if 'IPTEST_WORKING_DIR' in os.environ:
409 os.chdir(os.environ['IPTEST_WORKING_DIR'])
349
410
350 # We need a global ipython running in this process, but the special
411 # We need a global ipython running in this process, but the special
351 # in-process group spawns its own IPython kernels, so for *that* group we
412 # in-process group spawns its own IPython kernels, so for *that* group we
352 # must avoid also opening the global one (otherwise there's a conflict of
413 # must avoid also opening the global one (otherwise there's a conflict of
353 # singletons). Ultimately the solution to this problem is to refactor our
414 # singletons). Ultimately the solution to this problem is to refactor our
354 # assumptions about what needs to be a singleton and what doesn't (app
415 # assumptions about what needs to be a singleton and what doesn't (app
355 # objects should, individual shells shouldn't). But for now, this
416 # objects should, individual shells shouldn't). But for now, this
356 # workaround allows the test suite for the inprocess module to complete.
417 # workaround allows the test suite for the inprocess module to complete.
357 if not 'IPython.kernel.inprocess' in sys.argv:
418 if section.name != 'kernel.inprocess':
358 globalipapp.start_ipython()
419 globalipapp.start_ipython()
359
420
360 # Now nose can run
421 # Now nose can run
361 TestProgram(argv=argv, addplugins=plugins)
422 TestProgram(argv=argv, addplugins=plugins)
362
423
363
424
364 if __name__ == '__main__':
425 if __name__ == '__main__':
365 run_iptest()
426 run_iptest()
@@ -1,319 +1,275 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
2 """IPython Test Process Controller
3
3
4 This module runs one or more subprocesses which will actually run the IPython
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
5 test suite.
6
6
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2009-2011 The IPython Development Team
10 # Copyright (C) 2009-2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
19 from __future__ import print_function
20
20
21 import argparse
21 import multiprocessing.pool
22 import multiprocessing.pool
22 import os
23 import os
23 import signal
24 import signal
24 import sys
25 import sys
25 import subprocess
26 import subprocess
26 import tempfile
27 import time
27 import time
28
28
29 from .iptest import have, special_test_suites
29 from .iptest import have, test_group_names, test_sections
30 from IPython.utils import py3compat
30 from IPython.utils import py3compat
31 from IPython.utils.path import get_ipython_module_path
32 from IPython.utils.process import pycmd2argv
33 from IPython.utils.sysinfo import sys_info
31 from IPython.utils.sysinfo import sys_info
34 from IPython.utils.tempdir import TemporaryDirectory
32 from IPython.utils.tempdir import TemporaryDirectory
35
33
36
34
37 class IPTester(object):
35 class IPTestController(object):
38 """Call that calls iptest or trial in a subprocess.
36 """Run iptest in a subprocess
39 """
37 """
40 #: string, name of test runner that will be called
38 #: str, IPython test suite to be executed.
41 runner = None
39 section = None
42 #: list, parameters for test runner
40 #: list, command line arguments to be executed
43 params = None
41 cmd = None
44 #: list, arguments of system call to be made to call test runner
42 #: dict, extra environment variables to set for the subprocess
45 call_args = None
43 env = None
46 #: list, subprocesses we start (for cleanup)
44 #: list, TemporaryDirectory instances to clear up when the process finishes
47 processes = None
45 dirs = None
48 #: str, coverage xml output file
46 #: subprocess.Popen instance
49 coverage_xml = None
47 process = None
50 buffer_output = False
48 buffer_output = False
51
49
52 def __init__(self, runner='iptest', params=None):
50 def __init__(self, section):
53 """Create new test runner."""
51 """Create new test runner."""
54 if runner == 'iptest':
52 self.section = section
55 iptest_app = os.path.abspath(get_ipython_module_path('IPython.testing.iptest'))
53 self.cmd = [sys.executable, '-m', 'IPython.testing.iptest', section]
56 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
54 self.env = {}
57 else:
55 self.dirs = []
58 raise Exception('Not a valid test runner: %s' % repr(runner))
56 ipydir = TemporaryDirectory()
59 if params is None:
57 self.dirs.append(ipydir)
60 params = []
58 self.env['IPYTHONDIR'] = ipydir.name
61 if isinstance(params, str):
59 workingdir = TemporaryDirectory()
62 params = [params]
60 self.dirs.append(workingdir)
63 self.params = params
61 self.env['IPTEST_WORKING_DIR'] = workingdir.name
64
62
65 # Assemble call
63 def add_xunit(self):
66 self.call_args = self.runner+self.params
64 xunit_file = os.path.abspath(self.section + '.xunit.xml')
67
65 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
68 # Find the section we're testing (IPython.foo)
66
69 for sect in self.params:
67 def add_coverage(self, xml=True):
70 if sect.startswith('IPython') or sect in special_test_suites: break
68 self.cmd.extend(['--with-coverage', '--cover-package', self.section])
71 else:
69 if xml:
72 raise ValueError("Section not found", self.params)
70 coverage_xml = os.path.abspath(self.section + ".coverage.xml")
73
71 self.cmd.extend(['--cover-xml', '--cover-xml-file', coverage_xml])
74 if '--with-xunit' in self.call_args:
75
76 self.call_args.append('--xunit-file')
77 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
78 xunit_file = os.path.abspath(sect+'.xunit.xml')
79 if sys.platform == 'win32':
80 xunit_file = '"%s"' % xunit_file
81 self.call_args.append(xunit_file)
82
72
83 if '--with-xml-coverage' in self.call_args:
73
84 self.coverage_xml = os.path.abspath(sect+".coverage.xml")
74 def launch(self):
85 self.call_args.remove('--with-xml-coverage')
75 # print('*** ENV:', self.env) # dbg
86 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
76 # print('*** CMD:', self.cmd) # dbg
87
77 env = os.environ.copy()
88 # Store anything we start to clean up on deletion
78 env.update(self.env)
89 self.processes = []
79 output = subprocess.PIPE if self.buffer_output else None
90
80 self.process = subprocess.Popen(self.cmd, stdout=output,
91 def _run_cmd(self):
81 stderr=output, env=env)
92 with TemporaryDirectory() as IPYTHONDIR:
93 env = os.environ.copy()
94 env['IPYTHONDIR'] = IPYTHONDIR
95 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
96 output = subprocess.PIPE if self.buffer_output else None
97 subp = subprocess.Popen(self.call_args, stdout=output,
98 stderr=output, env=env)
99 self.processes.append(subp)
100 # If this fails, the process will be left in self.processes and
101 # cleaned up later, but if the wait call succeeds, then we can
102 # clear the stored process.
103 retcode = subp.wait()
104 self.processes.pop()
105 self.stdout = subp.stdout
106 self.stderr = subp.stderr
107 return retcode
108
82
109 def run(self):
83 def run(self):
110 """Run the stored commands"""
84 """Run the stored commands"""
111 try:
85 try:
112 retcode = self._run_cmd()
86 retcode = self._run_cmd()
113 except KeyboardInterrupt:
87 except KeyboardInterrupt:
114 return -signal.SIGINT
88 return -signal.SIGINT
115 except:
89 except:
116 import traceback
90 import traceback
117 traceback.print_exc()
91 traceback.print_exc()
118 return 1 # signal failure
92 return 1 # signal failure
119
93
120 if self.coverage_xml:
94 if self.coverage_xml:
121 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
95 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
122 return retcode
96 return retcode
123
97
124 def __del__(self):
98 def cleanup(self):
125 """Cleanup on exit by killing any leftover processes."""
99 """Cleanup on exit by killing any leftover processes."""
126 for subp in self.processes:
100 subp = self.process
127 if subp.poll() is not None:
101 if subp is None or (subp.poll() is not None):
128 continue # process is already dead
102 return # Process doesn't exist, or is already dead.
129
130 try:
131 print('Cleaning up stale PID: %d' % subp.pid)
132 subp.kill()
133 except: # (OSError, WindowsError) ?
134 # This is just a best effort, if we fail or the process was
135 # really gone, ignore it.
136 pass
137 else:
138 for i in range(10):
139 if subp.poll() is None:
140 time.sleep(0.1)
141 else:
142 break
143
144 if subp.poll() is None:
145 # The process did not die...
146 print('... failed. Manual cleanup may be required.')
147
148 def make_runners(inc_slow=False):
149 """Define the top-level packages that need to be tested.
150 """
151
103
152 # Packages to be tested via nose, that only depend on the stdlib
104 try:
153 nose_pkg_names = ['config', 'core', 'extensions', 'lib', 'terminal',
105 print('Cleaning up stale PID: %d' % subp.pid)
154 'testing', 'utils', 'nbformat']
106 subp.kill()
155
107 except: # (OSError, WindowsError) ?
156 if have['qt']:
108 # This is just a best effort, if we fail or the process was
157 nose_pkg_names.append('qt')
109 # really gone, ignore it.
110 pass
111 else:
112 for i in range(10):
113 if subp.poll() is None:
114 time.sleep(0.1)
115 else:
116 break
158
117
159 if have['tornado']:
118 if subp.poll() is None:
160 nose_pkg_names.append('html')
119 # The process did not die...
120 print('... failed. Manual cleanup may be required.')
161
121
162 if have['zmq']:
122 for td in self.dirs:
163 nose_pkg_names.insert(0, 'kernel')
123 td.cleanup()
164 nose_pkg_names.insert(1, 'kernel.inprocess')
165 if inc_slow:
166 nose_pkg_names.insert(0, 'parallel')
167
168 if all((have['pygments'], have['jinja2'], have['sphinx'])):
169 nose_pkg_names.append('nbconvert')
170
171 # For debugging this code, only load quick stuff
172 #nose_pkg_names = ['core', 'extensions'] # dbg
173
174 # Make fully qualified package names prepending 'IPython.' to our name lists
175 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
176
177 # Make runners
178 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
179
124
180 for name in special_test_suites:
125 __del__ = cleanup
181 runners.append((name, IPTester('iptest', params=name)))
126
182
127 def test_controllers_to_run(inc_slow=False):
183 return runners
128 """Returns an ordered list of IPTestController instances to be run."""
184
129 res = []
185 def do_run(x):
130 if not inc_slow:
186 print('IPython test group:',x[0])
131 test_sections['parallel'].enabled = False
187 ret = x[1].run()
132 for name in test_group_names:
188 return ret
133 if test_sections[name].will_run:
134 res.append(IPTestController(name))
135 return res
136
137 def do_run(controller):
138 try:
139 try:
140 controller.launch()
141 except Exception:
142 import traceback
143 traceback.print_exc()
144 return controller, 1 # signal failure
145
146 exitcode = controller.process.wait()
147 controller.cleanup()
148 return controller, exitcode
149
150 except KeyboardInterrupt:
151 controller.cleanup()
152 return controller, -signal.SIGINT
189
153
190 def report():
154 def report():
191 """Return a string with a summary report of test-related variables."""
155 """Return a string with a summary report of test-related variables."""
192
156
193 out = [ sys_info(), '\n']
157 out = [ sys_info(), '\n']
194
158
195 avail = []
159 avail = []
196 not_avail = []
160 not_avail = []
197
161
198 for k, is_avail in have.items():
162 for k, is_avail in have.items():
199 if is_avail:
163 if is_avail:
200 avail.append(k)
164 avail.append(k)
201 else:
165 else:
202 not_avail.append(k)
166 not_avail.append(k)
203
167
204 if avail:
168 if avail:
205 out.append('\nTools and libraries available at test time:\n')
169 out.append('\nTools and libraries available at test time:\n')
206 avail.sort()
170 avail.sort()
207 out.append(' ' + ' '.join(avail)+'\n')
171 out.append(' ' + ' '.join(avail)+'\n')
208
172
209 if not_avail:
173 if not_avail:
210 out.append('\nTools and libraries NOT available at test time:\n')
174 out.append('\nTools and libraries NOT available at test time:\n')
211 not_avail.sort()
175 not_avail.sort()
212 out.append(' ' + ' '.join(not_avail)+'\n')
176 out.append(' ' + ' '.join(not_avail)+'\n')
213
177
214 return ''.join(out)
178 return ''.join(out)
215
179
216 def run_iptestall(inc_slow=False, fast=False):
180 def run_iptestall(inc_slow=False, jobs=1, xunit=False, coverage=False):
217 """Run the entire IPython test suite by calling nose and trial.
181 """Run the entire IPython test suite by calling nose and trial.
218
182
219 This function constructs :class:`IPTester` instances for all IPython
183 This function constructs :class:`IPTester` instances for all IPython
220 modules and package and then runs each of them. This causes the modules
184 modules and package and then runs each of them. This causes the modules
221 and packages of IPython to be tested each in their own subprocess using
185 and packages of IPython to be tested each in their own subprocess using
222 nose.
186 nose.
223
187
224 Parameters
188 Parameters
225 ----------
189 ----------
226
190
227 inc_slow : bool, optional
191 inc_slow : bool, optional
228 Include slow tests, like IPython.parallel. By default, these tests aren't
192 Include slow tests, like IPython.parallel. By default, these tests aren't
229 run.
193 run.
230
194
231 fast : bool, option
195 fast : bool, option
232 Run the test suite in parallel, if True, using as many threads as there
196 Run the test suite in parallel, if True, using as many threads as there
233 are processors
197 are processors
234 """
198 """
235 if fast:
199 pool = multiprocessing.pool.ThreadPool(jobs)
236 p = multiprocessing.pool.ThreadPool()
200 if jobs != 1:
237 else:
201 IPTestController.buffer_output = True
238 p = multiprocessing.pool.ThreadPool(1)
239
240 runners = make_runners(inc_slow=inc_slow)
241
202
242 # Run the test runners in a temporary dir so we can nuke it when finished
203 controllers = test_controllers_to_run(inc_slow=inc_slow)
243 # to clean up any junk files left over by accident. This also makes it
244 # robust against being run in non-writeable directories by mistake, as the
245 # temp dir will always be user-writeable.
246 curdir = os.getcwdu()
247 testdir = tempfile.gettempdir()
248 os.chdir(testdir)
249
204
250 # Run all test runners, tracking execution time
205 # Run all test runners, tracking execution time
251 failed = []
206 failed = []
252 t_start = time.time()
207 t_start = time.time()
253
208
254 try:
209 print('*'*70)
255 all_res = p.map(do_run, runners)
210 for (controller, res) in pool.imap_unordered(do_run, controllers):
256 print('*'*70)
211 tgroup = 'IPython test group: ' + controller.section
257 for ((name, runner), res) in zip(runners, all_res):
212 res_string = 'OK' if res == 0 else 'FAILED'
258 tgroup = 'IPython test group: ' + name
213 res_string = res_string.rjust(70 - len(tgroup), '.')
259 res_string = 'OK' if res == 0 else 'FAILED'
214 print(tgroup + res_string)
260 res_string = res_string.rjust(70 - len(tgroup), '.')
215 if res:
261 print(tgroup + res_string)
216 failed.append(controller)
262 if res:
217 if res == -signal.SIGINT:
263 failed.append( (name, runner) )
218 print("Interrupted")
264 if res == -signal.SIGINT:
219 break
265 print("Interrupted")
220
266 break
267 finally:
268 os.chdir(curdir)
269 t_end = time.time()
221 t_end = time.time()
270 t_tests = t_end - t_start
222 t_tests = t_end - t_start
271 nrunners = len(runners)
223 nrunners = len(controllers)
272 nfail = len(failed)
224 nfail = len(failed)
273 # summarize results
225 # summarize results
274 print()
226 print()
275 print('*'*70)
227 print('*'*70)
276 print('Test suite completed for system with the following information:')
228 print('Test suite completed for system with the following information:')
277 print(report())
229 print(report())
278 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
230 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
279 print()
231 print()
280 print('Status:')
232 print('Status:')
281 if not failed:
233 if not failed:
282 print('OK')
234 print('OK')
283 else:
235 else:
284 # If anything went wrong, point out what command to rerun manually to
236 # If anything went wrong, point out what command to rerun manually to
285 # see the actual errors and individual summary
237 # see the actual errors and individual summary
286 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
238 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
287 for name, failed_runner in failed:
239 for controller in failed:
288 print('-'*40)
240 print('-'*40)
289 print('Runner failed:',name)
241 print('Runner failed:', controller.section)
290 print('You may wish to rerun this one individually, with:')
242 print('You may wish to rerun this one individually, with:')
291 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
243 failed_call_args = [py3compat.cast_unicode(x) for x in controller.cmd]
292 print(u' '.join(failed_call_args))
244 print(u' '.join(failed_call_args))
293 print()
245 print()
294 # Ensure that our exit code indicates failure
246 # Ensure that our exit code indicates failure
295 sys.exit(1)
247 sys.exit(1)
296
248
297
249
298 def main():
250 def main():
299 for arg in sys.argv[1:]:
251 if len(sys.argv) > 1 and (sys.argv[1] in test_sections):
300 if arg.startswith('IPython') or arg in special_test_suites:
252 from .iptest import run_iptest
301 from .iptest import run_iptest
253 # This is in-process
302 # This is in-process
254 run_iptest()
303 run_iptest()
255 return
304 else:
256
305 inc_slow = "--all" in sys.argv
257 parser = argparse.ArgumentParser(description='Run IPython test suite')
306 if inc_slow:
258 parser.add_argument('--all', action='store_true',
307 sys.argv.remove("--all")
259 help='Include slow tests not run by default.')
308
260 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
309 fast = "--fast" in sys.argv
261 help='Run test sections in parallel.')
310 if fast:
262 parser.add_argument('--xunit', action='store_true',
311 sys.argv.remove("--fast")
263 help='Produce Xunit XML results')
312 IPTester.buffer_output = True
264 parser.add_argument('--coverage', action='store_true',
313
265 help='Measure test coverage.')
314 # This starts subprocesses
266
315 run_iptestall(inc_slow=inc_slow, fast=fast)
267 options = parser.parse_args()
268
269 # This starts subprocesses
270 run_iptestall(inc_slow=options.all, jobs=options.fast,
271 xunit=options.xunit, coverage=options.coverage)
316
272
317
273
318 if __name__ == '__main__':
274 if __name__ == '__main__':
319 main()
275 main()
@@ -1,807 +1,761 b''
1 """Nose Plugin that supports IPython doctests.
1 """Nose Plugin that supports IPython doctests.
2
2
3 Limitations:
3 Limitations:
4
4
5 - When generating examples for use as doctests, make sure that you have
5 - When generating examples for use as doctests, make sure that you have
6 pretty-printing OFF. This can be done either by setting the
6 pretty-printing OFF. This can be done either by setting the
7 ``PlainTextFormatter.pprint`` option in your configuration file to False, or
7 ``PlainTextFormatter.pprint`` option in your configuration file to False, or
8 by interactively disabling it with %Pprint. This is required so that IPython
8 by interactively disabling it with %Pprint. This is required so that IPython
9 output matches that of normal Python, which is used by doctest for internal
9 output matches that of normal Python, which is used by doctest for internal
10 execution.
10 execution.
11
11
12 - Do not rely on specific prompt numbers for results (such as using
12 - Do not rely on specific prompt numbers for results (such as using
13 '_34==True', for example). For IPython tests run via an external process the
13 '_34==True', for example). For IPython tests run via an external process the
14 prompt numbers may be different, and IPython tests run as normal python code
14 prompt numbers may be different, and IPython tests run as normal python code
15 won't even have these special _NN variables set at all.
15 won't even have these special _NN variables set at all.
16 """
16 """
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Module imports
19 # Module imports
20
20
21 # From the standard library
21 # From the standard library
22 import __builtin__ as builtin_mod
22 import __builtin__ as builtin_mod
23 import commands
23 import commands
24 import doctest
24 import doctest
25 import inspect
25 import inspect
26 import logging
26 import logging
27 import os
27 import os
28 import re
28 import re
29 import sys
29 import sys
30 import traceback
30 import traceback
31 import unittest
31 import unittest
32
32
33 from inspect import getmodule
33 from inspect import getmodule
34 from StringIO import StringIO
34 from StringIO import StringIO
35
35
36 # We are overriding the default doctest runner, so we need to import a few
36 # We are overriding the default doctest runner, so we need to import a few
37 # things from doctest directly
37 # things from doctest directly
38 from doctest import (REPORTING_FLAGS, REPORT_ONLY_FIRST_FAILURE,
38 from doctest import (REPORTING_FLAGS, REPORT_ONLY_FIRST_FAILURE,
39 _unittest_reportflags, DocTestRunner,
39 _unittest_reportflags, DocTestRunner,
40 _extract_future_flags, pdb, _OutputRedirectingPdb,
40 _extract_future_flags, pdb, _OutputRedirectingPdb,
41 _exception_traceback,
41 _exception_traceback,
42 linecache)
42 linecache)
43
43
44 # Third-party modules
44 # Third-party modules
45 import nose.core
45 import nose.core
46
46
47 from nose.plugins import doctests, Plugin
47 from nose.plugins import doctests, Plugin
48 from nose.util import anyp, getpackage, test_address, resolve_name, tolist
48 from nose.util import anyp, getpackage, test_address, resolve_name, tolist
49
49
50 # Our own imports
50 # Our own imports
51
51
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53 # Module globals and other constants
53 # Module globals and other constants
54 #-----------------------------------------------------------------------------
54 #-----------------------------------------------------------------------------
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 #-----------------------------------------------------------------------------
59 #-----------------------------------------------------------------------------
60 # Classes and functions
60 # Classes and functions
61 #-----------------------------------------------------------------------------
61 #-----------------------------------------------------------------------------
62
62
63 def is_extension_module(filename):
63 def is_extension_module(filename):
64 """Return whether the given filename is an extension module.
64 """Return whether the given filename is an extension module.
65
65
66 This simply checks that the extension is either .so or .pyd.
66 This simply checks that the extension is either .so or .pyd.
67 """
67 """
68 return os.path.splitext(filename)[1].lower() in ('.so','.pyd')
68 return os.path.splitext(filename)[1].lower() in ('.so','.pyd')
69
69
70
70
71 class DocTestSkip(object):
71 class DocTestSkip(object):
72 """Object wrapper for doctests to be skipped."""
72 """Object wrapper for doctests to be skipped."""
73
73
74 ds_skip = """Doctest to skip.
74 ds_skip = """Doctest to skip.
75 >>> 1 #doctest: +SKIP
75 >>> 1 #doctest: +SKIP
76 """
76 """
77
77
78 def __init__(self,obj):
78 def __init__(self,obj):
79 self.obj = obj
79 self.obj = obj
80
80
81 def __getattribute__(self,key):
81 def __getattribute__(self,key):
82 if key == '__doc__':
82 if key == '__doc__':
83 return DocTestSkip.ds_skip
83 return DocTestSkip.ds_skip
84 else:
84 else:
85 return getattr(object.__getattribute__(self,'obj'),key)
85 return getattr(object.__getattribute__(self,'obj'),key)
86
86
87 # Modified version of the one in the stdlib, that fixes a python bug (doctests
87 # Modified version of the one in the stdlib, that fixes a python bug (doctests
88 # not found in extension modules, http://bugs.python.org/issue3158)
88 # not found in extension modules, http://bugs.python.org/issue3158)
89 class DocTestFinder(doctest.DocTestFinder):
89 class DocTestFinder(doctest.DocTestFinder):
90
90
91 def _from_module(self, module, object):
91 def _from_module(self, module, object):
92 """
92 """
93 Return true if the given object is defined in the given
93 Return true if the given object is defined in the given
94 module.
94 module.
95 """
95 """
96 if module is None:
96 if module is None:
97 return True
97 return True
98 elif inspect.isfunction(object):
98 elif inspect.isfunction(object):
99 return module.__dict__ is object.func_globals
99 return module.__dict__ is object.func_globals
100 elif inspect.isbuiltin(object):
100 elif inspect.isbuiltin(object):
101 return module.__name__ == object.__module__
101 return module.__name__ == object.__module__
102 elif inspect.isclass(object):
102 elif inspect.isclass(object):
103 return module.__name__ == object.__module__
103 return module.__name__ == object.__module__
104 elif inspect.ismethod(object):
104 elif inspect.ismethod(object):
105 # This one may be a bug in cython that fails to correctly set the
105 # This one may be a bug in cython that fails to correctly set the
106 # __module__ attribute of methods, but since the same error is easy
106 # __module__ attribute of methods, but since the same error is easy
107 # to make by extension code writers, having this safety in place
107 # to make by extension code writers, having this safety in place
108 # isn't such a bad idea
108 # isn't such a bad idea
109 return module.__name__ == object.im_class.__module__
109 return module.__name__ == object.im_class.__module__
110 elif inspect.getmodule(object) is not None:
110 elif inspect.getmodule(object) is not None:
111 return module is inspect.getmodule(object)
111 return module is inspect.getmodule(object)
112 elif hasattr(object, '__module__'):
112 elif hasattr(object, '__module__'):
113 return module.__name__ == object.__module__
113 return module.__name__ == object.__module__
114 elif isinstance(object, property):
114 elif isinstance(object, property):
115 return True # [XX] no way not be sure.
115 return True # [XX] no way not be sure.
116 else:
116 else:
117 raise ValueError("object must be a class or function")
117 raise ValueError("object must be a class or function")
118
118
119 def _find(self, tests, obj, name, module, source_lines, globs, seen):
119 def _find(self, tests, obj, name, module, source_lines, globs, seen):
120 """
120 """
121 Find tests for the given object and any contained objects, and
121 Find tests for the given object and any contained objects, and
122 add them to `tests`.
122 add them to `tests`.
123 """
123 """
124 #print '_find for:', obj, name, module # dbg
124 #print '_find for:', obj, name, module # dbg
125 if hasattr(obj,"skip_doctest"):
125 if hasattr(obj,"skip_doctest"):
126 #print 'SKIPPING DOCTEST FOR:',obj # dbg
126 #print 'SKIPPING DOCTEST FOR:',obj # dbg
127 obj = DocTestSkip(obj)
127 obj = DocTestSkip(obj)
128
128
129 doctest.DocTestFinder._find(self,tests, obj, name, module,
129 doctest.DocTestFinder._find(self,tests, obj, name, module,
130 source_lines, globs, seen)
130 source_lines, globs, seen)
131
131
132 # Below we re-run pieces of the above method with manual modifications,
132 # Below we re-run pieces of the above method with manual modifications,
133 # because the original code is buggy and fails to correctly identify
133 # because the original code is buggy and fails to correctly identify
134 # doctests in extension modules.
134 # doctests in extension modules.
135
135
136 # Local shorthands
136 # Local shorthands
137 from inspect import isroutine, isclass, ismodule
137 from inspect import isroutine, isclass, ismodule
138
138
139 # Look for tests in a module's contained objects.
139 # Look for tests in a module's contained objects.
140 if inspect.ismodule(obj) and self._recurse:
140 if inspect.ismodule(obj) and self._recurse:
141 for valname, val in obj.__dict__.items():
141 for valname, val in obj.__dict__.items():
142 valname1 = '%s.%s' % (name, valname)
142 valname1 = '%s.%s' % (name, valname)
143 if ( (isroutine(val) or isclass(val))
143 if ( (isroutine(val) or isclass(val))
144 and self._from_module(module, val) ):
144 and self._from_module(module, val) ):
145
145
146 self._find(tests, val, valname1, module, source_lines,
146 self._find(tests, val, valname1, module, source_lines,
147 globs, seen)
147 globs, seen)
148
148
149 # Look for tests in a class's contained objects.
149 # Look for tests in a class's contained objects.
150 if inspect.isclass(obj) and self._recurse:
150 if inspect.isclass(obj) and self._recurse:
151 #print 'RECURSE into class:',obj # dbg
151 #print 'RECURSE into class:',obj # dbg
152 for valname, val in obj.__dict__.items():
152 for valname, val in obj.__dict__.items():
153 # Special handling for staticmethod/classmethod.
153 # Special handling for staticmethod/classmethod.
154 if isinstance(val, staticmethod):
154 if isinstance(val, staticmethod):
155 val = getattr(obj, valname)
155 val = getattr(obj, valname)
156 if isinstance(val, classmethod):
156 if isinstance(val, classmethod):
157 val = getattr(obj, valname).im_func
157 val = getattr(obj, valname).im_func
158
158
159 # Recurse to methods, properties, and nested classes.
159 # Recurse to methods, properties, and nested classes.
160 if ((inspect.isfunction(val) or inspect.isclass(val) or
160 if ((inspect.isfunction(val) or inspect.isclass(val) or
161 inspect.ismethod(val) or
161 inspect.ismethod(val) or
162 isinstance(val, property)) and
162 isinstance(val, property)) and
163 self._from_module(module, val)):
163 self._from_module(module, val)):
164 valname = '%s.%s' % (name, valname)
164 valname = '%s.%s' % (name, valname)
165 self._find(tests, val, valname, module, source_lines,
165 self._find(tests, val, valname, module, source_lines,
166 globs, seen)
166 globs, seen)
167
167
168
168
169 class IPDoctestOutputChecker(doctest.OutputChecker):
169 class IPDoctestOutputChecker(doctest.OutputChecker):
170 """Second-chance checker with support for random tests.
170 """Second-chance checker with support for random tests.
171
171
172 If the default comparison doesn't pass, this checker looks in the expected
172 If the default comparison doesn't pass, this checker looks in the expected
173 output string for flags that tell us to ignore the output.
173 output string for flags that tell us to ignore the output.
174 """
174 """
175
175
176 random_re = re.compile(r'#\s*random\s+')
176 random_re = re.compile(r'#\s*random\s+')
177
177
178 def check_output(self, want, got, optionflags):
178 def check_output(self, want, got, optionflags):
179 """Check output, accepting special markers embedded in the output.
179 """Check output, accepting special markers embedded in the output.
180
180
181 If the output didn't pass the default validation but the special string
181 If the output didn't pass the default validation but the special string
182 '#random' is included, we accept it."""
182 '#random' is included, we accept it."""
183
183
184 # Let the original tester verify first, in case people have valid tests
184 # Let the original tester verify first, in case people have valid tests
185 # that happen to have a comment saying '#random' embedded in.
185 # that happen to have a comment saying '#random' embedded in.
186 ret = doctest.OutputChecker.check_output(self, want, got,
186 ret = doctest.OutputChecker.check_output(self, want, got,
187 optionflags)
187 optionflags)
188 if not ret and self.random_re.search(want):
188 if not ret and self.random_re.search(want):
189 #print >> sys.stderr, 'RANDOM OK:',want # dbg
189 #print >> sys.stderr, 'RANDOM OK:',want # dbg
190 return True
190 return True
191
191
192 return ret
192 return ret
193
193
194
194
195 class DocTestCase(doctests.DocTestCase):
195 class DocTestCase(doctests.DocTestCase):
196 """Proxy for DocTestCase: provides an address() method that
196 """Proxy for DocTestCase: provides an address() method that
197 returns the correct address for the doctest case. Otherwise
197 returns the correct address for the doctest case. Otherwise
198 acts as a proxy to the test case. To provide hints for address(),
198 acts as a proxy to the test case. To provide hints for address(),
199 an obj may also be passed -- this will be used as the test object
199 an obj may also be passed -- this will be used as the test object
200 for purposes of determining the test address, if it is provided.
200 for purposes of determining the test address, if it is provided.
201 """
201 """
202
202
203 # Note: this method was taken from numpy's nosetester module.
203 # Note: this method was taken from numpy's nosetester module.
204
204
205 # Subclass nose.plugins.doctests.DocTestCase to work around a bug in
205 # Subclass nose.plugins.doctests.DocTestCase to work around a bug in
206 # its constructor that blocks non-default arguments from being passed
206 # its constructor that blocks non-default arguments from being passed
207 # down into doctest.DocTestCase
207 # down into doctest.DocTestCase
208
208
209 def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
209 def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
210 checker=None, obj=None, result_var='_'):
210 checker=None, obj=None, result_var='_'):
211 self._result_var = result_var
211 self._result_var = result_var
212 doctests.DocTestCase.__init__(self, test,
212 doctests.DocTestCase.__init__(self, test,
213 optionflags=optionflags,
213 optionflags=optionflags,
214 setUp=setUp, tearDown=tearDown,
214 setUp=setUp, tearDown=tearDown,
215 checker=checker)
215 checker=checker)
216 # Now we must actually copy the original constructor from the stdlib
216 # Now we must actually copy the original constructor from the stdlib
217 # doctest class, because we can't call it directly and a bug in nose
217 # doctest class, because we can't call it directly and a bug in nose
218 # means it never gets passed the right arguments.
218 # means it never gets passed the right arguments.
219
219
220 self._dt_optionflags = optionflags
220 self._dt_optionflags = optionflags
221 self._dt_checker = checker
221 self._dt_checker = checker
222 self._dt_test = test
222 self._dt_test = test
223 self._dt_test_globs_ori = test.globs
223 self._dt_test_globs_ori = test.globs
224 self._dt_setUp = setUp
224 self._dt_setUp = setUp
225 self._dt_tearDown = tearDown
225 self._dt_tearDown = tearDown
226
226
227 # XXX - store this runner once in the object!
227 # XXX - store this runner once in the object!
228 runner = IPDocTestRunner(optionflags=optionflags,
228 runner = IPDocTestRunner(optionflags=optionflags,
229 checker=checker, verbose=False)
229 checker=checker, verbose=False)
230 self._dt_runner = runner
230 self._dt_runner = runner
231
231
232
232
233 # Each doctest should remember the directory it was loaded from, so
233 # Each doctest should remember the directory it was loaded from, so
234 # things like %run work without too many contortions
234 # things like %run work without too many contortions
235 self._ori_dir = os.path.dirname(test.filename)
235 self._ori_dir = os.path.dirname(test.filename)
236
236
237 # Modified runTest from the default stdlib
237 # Modified runTest from the default stdlib
238 def runTest(self):
238 def runTest(self):
239 test = self._dt_test
239 test = self._dt_test
240 runner = self._dt_runner
240 runner = self._dt_runner
241
241
242 old = sys.stdout
242 old = sys.stdout
243 new = StringIO()
243 new = StringIO()
244 optionflags = self._dt_optionflags
244 optionflags = self._dt_optionflags
245
245
246 if not (optionflags & REPORTING_FLAGS):
246 if not (optionflags & REPORTING_FLAGS):
247 # The option flags don't include any reporting flags,
247 # The option flags don't include any reporting flags,
248 # so add the default reporting flags
248 # so add the default reporting flags
249 optionflags |= _unittest_reportflags
249 optionflags |= _unittest_reportflags
250
250
251 try:
251 try:
252 # Save our current directory and switch out to the one where the
252 # Save our current directory and switch out to the one where the
253 # test was originally created, in case another doctest did a
253 # test was originally created, in case another doctest did a
254 # directory change. We'll restore this in the finally clause.
254 # directory change. We'll restore this in the finally clause.
255 curdir = os.getcwdu()
255 curdir = os.getcwdu()
256 #print 'runTest in dir:', self._ori_dir # dbg
256 #print 'runTest in dir:', self._ori_dir # dbg
257 os.chdir(self._ori_dir)
257 os.chdir(self._ori_dir)
258
258
259 runner.DIVIDER = "-"*70
259 runner.DIVIDER = "-"*70
260 failures, tries = runner.run(test,out=new.write,
260 failures, tries = runner.run(test,out=new.write,
261 clear_globs=False)
261 clear_globs=False)
262 finally:
262 finally:
263 sys.stdout = old
263 sys.stdout = old
264 os.chdir(curdir)
264 os.chdir(curdir)
265
265
266 if failures:
266 if failures:
267 raise self.failureException(self.format_failure(new.getvalue()))
267 raise self.failureException(self.format_failure(new.getvalue()))
268
268
269 def setUp(self):
269 def setUp(self):
270 """Modified test setup that syncs with ipython namespace"""
270 """Modified test setup that syncs with ipython namespace"""
271 #print "setUp test", self._dt_test.examples # dbg
271 #print "setUp test", self._dt_test.examples # dbg
272 if isinstance(self._dt_test.examples[0], IPExample):
272 if isinstance(self._dt_test.examples[0], IPExample):
273 # for IPython examples *only*, we swap the globals with the ipython
273 # for IPython examples *only*, we swap the globals with the ipython
274 # namespace, after updating it with the globals (which doctest
274 # namespace, after updating it with the globals (which doctest
275 # fills with the necessary info from the module being tested).
275 # fills with the necessary info from the module being tested).
276 self.user_ns_orig = {}
276 self.user_ns_orig = {}
277 self.user_ns_orig.update(_ip.user_ns)
277 self.user_ns_orig.update(_ip.user_ns)
278 _ip.user_ns.update(self._dt_test.globs)
278 _ip.user_ns.update(self._dt_test.globs)
279 # We must remove the _ key in the namespace, so that Python's
279 # We must remove the _ key in the namespace, so that Python's
280 # doctest code sets it naturally
280 # doctest code sets it naturally
281 _ip.user_ns.pop('_', None)
281 _ip.user_ns.pop('_', None)
282 _ip.user_ns['__builtins__'] = builtin_mod
282 _ip.user_ns['__builtins__'] = builtin_mod
283 self._dt_test.globs = _ip.user_ns
283 self._dt_test.globs = _ip.user_ns
284
284
285 super(DocTestCase, self).setUp()
285 super(DocTestCase, self).setUp()
286
286
287 def tearDown(self):
287 def tearDown(self):
288
288
289 # Undo the test.globs reassignment we made, so that the parent class
289 # Undo the test.globs reassignment we made, so that the parent class
290 # teardown doesn't destroy the ipython namespace
290 # teardown doesn't destroy the ipython namespace
291 if isinstance(self._dt_test.examples[0], IPExample):
291 if isinstance(self._dt_test.examples[0], IPExample):
292 self._dt_test.globs = self._dt_test_globs_ori
292 self._dt_test.globs = self._dt_test_globs_ori
293 _ip.user_ns.clear()
293 _ip.user_ns.clear()
294 _ip.user_ns.update(self.user_ns_orig)
294 _ip.user_ns.update(self.user_ns_orig)
295
295
296 # XXX - fperez: I am not sure if this is truly a bug in nose 0.11, but
296 # XXX - fperez: I am not sure if this is truly a bug in nose 0.11, but
297 # it does look like one to me: its tearDown method tries to run
297 # it does look like one to me: its tearDown method tries to run
298 #
298 #
299 # delattr(__builtin__, self._result_var)
299 # delattr(__builtin__, self._result_var)
300 #
300 #
301 # without checking that the attribute really is there; it implicitly
301 # without checking that the attribute really is there; it implicitly
302 # assumes it should have been set via displayhook. But if the
302 # assumes it should have been set via displayhook. But if the
303 # displayhook was never called, this doesn't necessarily happen. I
303 # displayhook was never called, this doesn't necessarily happen. I
304 # haven't been able to find a little self-contained example outside of
304 # haven't been able to find a little self-contained example outside of
305 # ipython that would show the problem so I can report it to the nose
305 # ipython that would show the problem so I can report it to the nose
306 # team, but it does happen a lot in our code.
306 # team, but it does happen a lot in our code.
307 #
307 #
308 # So here, we just protect as narrowly as possible by trapping an
308 # So here, we just protect as narrowly as possible by trapping an
309 # attribute error whose message would be the name of self._result_var,
309 # attribute error whose message would be the name of self._result_var,
310 # and letting any other error propagate.
310 # and letting any other error propagate.
311 try:
311 try:
312 super(DocTestCase, self).tearDown()
312 super(DocTestCase, self).tearDown()
313 except AttributeError as exc:
313 except AttributeError as exc:
314 if exc.args[0] != self._result_var:
314 if exc.args[0] != self._result_var:
315 raise
315 raise
316
316
317
317
318 # A simple subclassing of the original with a different class name, so we can
318 # A simple subclassing of the original with a different class name, so we can
319 # distinguish and treat differently IPython examples from pure python ones.
319 # distinguish and treat differently IPython examples from pure python ones.
320 class IPExample(doctest.Example): pass
320 class IPExample(doctest.Example): pass
321
321
322
322
323 class IPExternalExample(doctest.Example):
323 class IPExternalExample(doctest.Example):
324 """Doctest examples to be run in an external process."""
324 """Doctest examples to be run in an external process."""
325
325
326 def __init__(self, source, want, exc_msg=None, lineno=0, indent=0,
326 def __init__(self, source, want, exc_msg=None, lineno=0, indent=0,
327 options=None):
327 options=None):
328 # Parent constructor
328 # Parent constructor
329 doctest.Example.__init__(self,source,want,exc_msg,lineno,indent,options)
329 doctest.Example.__init__(self,source,want,exc_msg,lineno,indent,options)
330
330
331 # An EXTRA newline is needed to prevent pexpect hangs
331 # An EXTRA newline is needed to prevent pexpect hangs
332 self.source += '\n'
332 self.source += '\n'
333
333
334
334
335 class IPDocTestParser(doctest.DocTestParser):
335 class IPDocTestParser(doctest.DocTestParser):
336 """
336 """
337 A class used to parse strings containing doctest examples.
337 A class used to parse strings containing doctest examples.
338
338
339 Note: This is a version modified to properly recognize IPython input and
339 Note: This is a version modified to properly recognize IPython input and
340 convert any IPython examples into valid Python ones.
340 convert any IPython examples into valid Python ones.
341 """
341 """
342 # This regular expression is used to find doctest examples in a
342 # This regular expression is used to find doctest examples in a
343 # string. It defines three groups: `source` is the source code
343 # string. It defines three groups: `source` is the source code
344 # (including leading indentation and prompts); `indent` is the
344 # (including leading indentation and prompts); `indent` is the
345 # indentation of the first (PS1) line of the source code; and
345 # indentation of the first (PS1) line of the source code; and
346 # `want` is the expected output (including leading indentation).
346 # `want` is the expected output (including leading indentation).
347
347
348 # Classic Python prompts or default IPython ones
348 # Classic Python prompts or default IPython ones
349 _PS1_PY = r'>>>'
349 _PS1_PY = r'>>>'
350 _PS2_PY = r'\.\.\.'
350 _PS2_PY = r'\.\.\.'
351
351
352 _PS1_IP = r'In\ \[\d+\]:'
352 _PS1_IP = r'In\ \[\d+\]:'
353 _PS2_IP = r'\ \ \ \.\.\.+:'
353 _PS2_IP = r'\ \ \ \.\.\.+:'
354
354
355 _RE_TPL = r'''
355 _RE_TPL = r'''
356 # Source consists of a PS1 line followed by zero or more PS2 lines.
356 # Source consists of a PS1 line followed by zero or more PS2 lines.
357 (?P<source>
357 (?P<source>
358 (?:^(?P<indent> [ ]*) (?P<ps1> %s) .*) # PS1 line
358 (?:^(?P<indent> [ ]*) (?P<ps1> %s) .*) # PS1 line
359 (?:\n [ ]* (?P<ps2> %s) .*)*) # PS2 lines
359 (?:\n [ ]* (?P<ps2> %s) .*)*) # PS2 lines
360 \n? # a newline
360 \n? # a newline
361 # Want consists of any non-blank lines that do not start with PS1.
361 # Want consists of any non-blank lines that do not start with PS1.
362 (?P<want> (?:(?![ ]*$) # Not a blank line
362 (?P<want> (?:(?![ ]*$) # Not a blank line
363 (?![ ]*%s) # Not a line starting with PS1
363 (?![ ]*%s) # Not a line starting with PS1
364 (?![ ]*%s) # Not a line starting with PS2
364 (?![ ]*%s) # Not a line starting with PS2
365 .*$\n? # But any other line
365 .*$\n? # But any other line
366 )*)
366 )*)
367 '''
367 '''
368
368
369 _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
369 _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
370 re.MULTILINE | re.VERBOSE)
370 re.MULTILINE | re.VERBOSE)
371
371
372 _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
372 _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
373 re.MULTILINE | re.VERBOSE)
373 re.MULTILINE | re.VERBOSE)
374
374
375 # Mark a test as being fully random. In this case, we simply append the
375 # Mark a test as being fully random. In this case, we simply append the
376 # random marker ('#random') to each individual example's output. This way
376 # random marker ('#random') to each individual example's output. This way
377 # we don't need to modify any other code.
377 # we don't need to modify any other code.
378 _RANDOM_TEST = re.compile(r'#\s*all-random\s+')
378 _RANDOM_TEST = re.compile(r'#\s*all-random\s+')
379
379
380 # Mark tests to be executed in an external process - currently unsupported.
380 # Mark tests to be executed in an external process - currently unsupported.
381 _EXTERNAL_IP = re.compile(r'#\s*ipdoctest:\s*EXTERNAL')
381 _EXTERNAL_IP = re.compile(r'#\s*ipdoctest:\s*EXTERNAL')
382
382
383 def ip2py(self,source):
383 def ip2py(self,source):
384 """Convert input IPython source into valid Python."""
384 """Convert input IPython source into valid Python."""
385 block = _ip.input_transformer_manager.transform_cell(source)
385 block = _ip.input_transformer_manager.transform_cell(source)
386 if len(block.splitlines()) == 1:
386 if len(block.splitlines()) == 1:
387 return _ip.prefilter(block)
387 return _ip.prefilter(block)
388 else:
388 else:
389 return block
389 return block
390
390
391 def parse(self, string, name='<string>'):
391 def parse(self, string, name='<string>'):
392 """
392 """
393 Divide the given string into examples and intervening text,
393 Divide the given string into examples and intervening text,
394 and return them as a list of alternating Examples and strings.
394 and return them as a list of alternating Examples and strings.
395 Line numbers for the Examples are 0-based. The optional
395 Line numbers for the Examples are 0-based. The optional
396 argument `name` is a name identifying this string, and is only
396 argument `name` is a name identifying this string, and is only
397 used for error messages.
397 used for error messages.
398 """
398 """
399
399
400 #print 'Parse string:\n',string # dbg
400 #print 'Parse string:\n',string # dbg
401
401
402 string = string.expandtabs()
402 string = string.expandtabs()
403 # If all lines begin with the same indentation, then strip it.
403 # If all lines begin with the same indentation, then strip it.
404 min_indent = self._min_indent(string)
404 min_indent = self._min_indent(string)
405 if min_indent > 0:
405 if min_indent > 0:
406 string = '\n'.join([l[min_indent:] for l in string.split('\n')])
406 string = '\n'.join([l[min_indent:] for l in string.split('\n')])
407
407
408 output = []
408 output = []
409 charno, lineno = 0, 0
409 charno, lineno = 0, 0
410
410
411 # We make 'all random' tests by adding the '# random' mark to every
411 # We make 'all random' tests by adding the '# random' mark to every
412 # block of output in the test.
412 # block of output in the test.
413 if self._RANDOM_TEST.search(string):
413 if self._RANDOM_TEST.search(string):
414 random_marker = '\n# random'
414 random_marker = '\n# random'
415 else:
415 else:
416 random_marker = ''
416 random_marker = ''
417
417
418 # Whether to convert the input from ipython to python syntax
418 # Whether to convert the input from ipython to python syntax
419 ip2py = False
419 ip2py = False
420 # Find all doctest examples in the string. First, try them as Python
420 # Find all doctest examples in the string. First, try them as Python
421 # examples, then as IPython ones
421 # examples, then as IPython ones
422 terms = list(self._EXAMPLE_RE_PY.finditer(string))
422 terms = list(self._EXAMPLE_RE_PY.finditer(string))
423 if terms:
423 if terms:
424 # Normal Python example
424 # Normal Python example
425 #print '-'*70 # dbg
425 #print '-'*70 # dbg
426 #print 'PyExample, Source:\n',string # dbg
426 #print 'PyExample, Source:\n',string # dbg
427 #print '-'*70 # dbg
427 #print '-'*70 # dbg
428 Example = doctest.Example
428 Example = doctest.Example
429 else:
429 else:
430 # It's an ipython example. Note that IPExamples are run
430 # It's an ipython example. Note that IPExamples are run
431 # in-process, so their syntax must be turned into valid python.
431 # in-process, so their syntax must be turned into valid python.
432 # IPExternalExamples are run out-of-process (via pexpect) so they
432 # IPExternalExamples are run out-of-process (via pexpect) so they
433 # don't need any filtering (a real ipython will be executing them).
433 # don't need any filtering (a real ipython will be executing them).
434 terms = list(self._EXAMPLE_RE_IP.finditer(string))
434 terms = list(self._EXAMPLE_RE_IP.finditer(string))
435 if self._EXTERNAL_IP.search(string):
435 if self._EXTERNAL_IP.search(string):
436 #print '-'*70 # dbg
436 #print '-'*70 # dbg
437 #print 'IPExternalExample, Source:\n',string # dbg
437 #print 'IPExternalExample, Source:\n',string # dbg
438 #print '-'*70 # dbg
438 #print '-'*70 # dbg
439 Example = IPExternalExample
439 Example = IPExternalExample
440 else:
440 else:
441 #print '-'*70 # dbg
441 #print '-'*70 # dbg
442 #print 'IPExample, Source:\n',string # dbg
442 #print 'IPExample, Source:\n',string # dbg
443 #print '-'*70 # dbg
443 #print '-'*70 # dbg
444 Example = IPExample
444 Example = IPExample
445 ip2py = True
445 ip2py = True
446
446
447 for m in terms:
447 for m in terms:
448 # Add the pre-example text to `output`.
448 # Add the pre-example text to `output`.
449 output.append(string[charno:m.start()])
449 output.append(string[charno:m.start()])
450 # Update lineno (lines before this example)
450 # Update lineno (lines before this example)
451 lineno += string.count('\n', charno, m.start())
451 lineno += string.count('\n', charno, m.start())
452 # Extract info from the regexp match.
452 # Extract info from the regexp match.
453 (source, options, want, exc_msg) = \
453 (source, options, want, exc_msg) = \
454 self._parse_example(m, name, lineno,ip2py)
454 self._parse_example(m, name, lineno,ip2py)
455
455
456 # Append the random-output marker (it defaults to empty in most
456 # Append the random-output marker (it defaults to empty in most
457 # cases, it's only non-empty for 'all-random' tests):
457 # cases, it's only non-empty for 'all-random' tests):
458 want += random_marker
458 want += random_marker
459
459
460 if Example is IPExternalExample:
460 if Example is IPExternalExample:
461 options[doctest.NORMALIZE_WHITESPACE] = True
461 options[doctest.NORMALIZE_WHITESPACE] = True
462 want += '\n'
462 want += '\n'
463
463
464 # Create an Example, and add it to the list.
464 # Create an Example, and add it to the list.
465 if not self._IS_BLANK_OR_COMMENT(source):
465 if not self._IS_BLANK_OR_COMMENT(source):
466 output.append(Example(source, want, exc_msg,
466 output.append(Example(source, want, exc_msg,
467 lineno=lineno,
467 lineno=lineno,
468 indent=min_indent+len(m.group('indent')),
468 indent=min_indent+len(m.group('indent')),
469 options=options))
469 options=options))
470 # Update lineno (lines inside this example)
470 # Update lineno (lines inside this example)
471 lineno += string.count('\n', m.start(), m.end())
471 lineno += string.count('\n', m.start(), m.end())
472 # Update charno.
472 # Update charno.
473 charno = m.end()
473 charno = m.end()
474 # Add any remaining post-example text to `output`.
474 # Add any remaining post-example text to `output`.
475 output.append(string[charno:])
475 output.append(string[charno:])
476 return output
476 return output
477
477
478 def _parse_example(self, m, name, lineno,ip2py=False):
478 def _parse_example(self, m, name, lineno,ip2py=False):
479 """
479 """
480 Given a regular expression match from `_EXAMPLE_RE` (`m`),
480 Given a regular expression match from `_EXAMPLE_RE` (`m`),
481 return a pair `(source, want)`, where `source` is the matched
481 return a pair `(source, want)`, where `source` is the matched
482 example's source code (with prompts and indentation stripped);
482 example's source code (with prompts and indentation stripped);
483 and `want` is the example's expected output (with indentation
483 and `want` is the example's expected output (with indentation
484 stripped).
484 stripped).
485
485
486 `name` is the string's name, and `lineno` is the line number
486 `name` is the string's name, and `lineno` is the line number
487 where the example starts; both are used for error messages.
487 where the example starts; both are used for error messages.
488
488
489 Optional:
489 Optional:
490 `ip2py`: if true, filter the input via IPython to convert the syntax
490 `ip2py`: if true, filter the input via IPython to convert the syntax
491 into valid python.
491 into valid python.
492 """
492 """
493
493
494 # Get the example's indentation level.
494 # Get the example's indentation level.
495 indent = len(m.group('indent'))
495 indent = len(m.group('indent'))
496
496
497 # Divide source into lines; check that they're properly
497 # Divide source into lines; check that they're properly
498 # indented; and then strip their indentation & prompts.
498 # indented; and then strip their indentation & prompts.
499 source_lines = m.group('source').split('\n')
499 source_lines = m.group('source').split('\n')
500
500
501 # We're using variable-length input prompts
501 # We're using variable-length input prompts
502 ps1 = m.group('ps1')
502 ps1 = m.group('ps1')
503 ps2 = m.group('ps2')
503 ps2 = m.group('ps2')
504 ps1_len = len(ps1)
504 ps1_len = len(ps1)
505
505
506 self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
506 self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
507 if ps2:
507 if ps2:
508 self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
508 self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
509
509
510 source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
510 source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
511
511
512 if ip2py:
512 if ip2py:
513 # Convert source input from IPython into valid Python syntax
513 # Convert source input from IPython into valid Python syntax
514 source = self.ip2py(source)
514 source = self.ip2py(source)
515
515
516 # Divide want into lines; check that it's properly indented; and
516 # Divide want into lines; check that it's properly indented; and
517 # then strip the indentation. Spaces before the last newline should
517 # then strip the indentation. Spaces before the last newline should
518 # be preserved, so plain rstrip() isn't good enough.
518 # be preserved, so plain rstrip() isn't good enough.
519 want = m.group('want')
519 want = m.group('want')
520 want_lines = want.split('\n')
520 want_lines = want.split('\n')
521 if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
521 if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
522 del want_lines[-1] # forget final newline & spaces after it
522 del want_lines[-1] # forget final newline & spaces after it
523 self._check_prefix(want_lines, ' '*indent, name,
523 self._check_prefix(want_lines, ' '*indent, name,
524 lineno + len(source_lines))
524 lineno + len(source_lines))
525
525
526 # Remove ipython output prompt that might be present in the first line
526 # Remove ipython output prompt that might be present in the first line
527 want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
527 want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
528
528
529 want = '\n'.join([wl[indent:] for wl in want_lines])
529 want = '\n'.join([wl[indent:] for wl in want_lines])
530
530
531 # If `want` contains a traceback message, then extract it.
531 # If `want` contains a traceback message, then extract it.
532 m = self._EXCEPTION_RE.match(want)
532 m = self._EXCEPTION_RE.match(want)
533 if m:
533 if m:
534 exc_msg = m.group('msg')
534 exc_msg = m.group('msg')
535 else:
535 else:
536 exc_msg = None
536 exc_msg = None
537
537
538 # Extract options from the source.
538 # Extract options from the source.
539 options = self._find_options(source, name, lineno)
539 options = self._find_options(source, name, lineno)
540
540
541 return source, options, want, exc_msg
541 return source, options, want, exc_msg
542
542
543 def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
543 def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
544 """
544 """
545 Given the lines of a source string (including prompts and
545 Given the lines of a source string (including prompts and
546 leading indentation), check to make sure that every prompt is
546 leading indentation), check to make sure that every prompt is
547 followed by a space character. If any line is not followed by
547 followed by a space character. If any line is not followed by
548 a space character, then raise ValueError.
548 a space character, then raise ValueError.
549
549
550 Note: IPython-modified version which takes the input prompt length as a
550 Note: IPython-modified version which takes the input prompt length as a
551 parameter, so that prompts of variable length can be dealt with.
551 parameter, so that prompts of variable length can be dealt with.
552 """
552 """
553 space_idx = indent+ps1_len
553 space_idx = indent+ps1_len
554 min_len = space_idx+1
554 min_len = space_idx+1
555 for i, line in enumerate(lines):
555 for i, line in enumerate(lines):
556 if len(line) >= min_len and line[space_idx] != ' ':
556 if len(line) >= min_len and line[space_idx] != ' ':
557 raise ValueError('line %r of the docstring for %s '
557 raise ValueError('line %r of the docstring for %s '
558 'lacks blank after %s: %r' %
558 'lacks blank after %s: %r' %
559 (lineno+i+1, name,
559 (lineno+i+1, name,
560 line[indent:space_idx], line))
560 line[indent:space_idx], line))
561
561
562
562
563 SKIP = doctest.register_optionflag('SKIP')
563 SKIP = doctest.register_optionflag('SKIP')
564
564
565
565
566 class IPDocTestRunner(doctest.DocTestRunner,object):
566 class IPDocTestRunner(doctest.DocTestRunner,object):
567 """Test runner that synchronizes the IPython namespace with test globals.
567 """Test runner that synchronizes the IPython namespace with test globals.
568 """
568 """
569
569
570 def run(self, test, compileflags=None, out=None, clear_globs=True):
570 def run(self, test, compileflags=None, out=None, clear_globs=True):
571
571
572 # Hack: ipython needs access to the execution context of the example,
572 # Hack: ipython needs access to the execution context of the example,
573 # so that it can propagate user variables loaded by %run into
573 # so that it can propagate user variables loaded by %run into
574 # test.globs. We put them here into our modified %run as a function
574 # test.globs. We put them here into our modified %run as a function
575 # attribute. Our new %run will then only make the namespace update
575 # attribute. Our new %run will then only make the namespace update
576 # when called (rather than unconconditionally updating test.globs here
576 # when called (rather than unconconditionally updating test.globs here
577 # for all examples, most of which won't be calling %run anyway).
577 # for all examples, most of which won't be calling %run anyway).
578 #_ip._ipdoctest_test_globs = test.globs
578 #_ip._ipdoctest_test_globs = test.globs
579 #_ip._ipdoctest_test_filename = test.filename
579 #_ip._ipdoctest_test_filename = test.filename
580
580
581 test.globs.update(_ip.user_ns)
581 test.globs.update(_ip.user_ns)
582
582
583 return super(IPDocTestRunner,self).run(test,
583 return super(IPDocTestRunner,self).run(test,
584 compileflags,out,clear_globs)
584 compileflags,out,clear_globs)
585
585
586
586
587 class DocFileCase(doctest.DocFileCase):
587 class DocFileCase(doctest.DocFileCase):
588 """Overrides to provide filename
588 """Overrides to provide filename
589 """
589 """
590 def address(self):
590 def address(self):
591 return (self._dt_test.filename, None, None)
591 return (self._dt_test.filename, None, None)
592
592
593
593
594 class ExtensionDoctest(doctests.Doctest):
594 class ExtensionDoctest(doctests.Doctest):
595 """Nose Plugin that supports doctests in extension modules.
595 """Nose Plugin that supports doctests in extension modules.
596 """
596 """
597 name = 'extdoctest' # call nosetests with --with-extdoctest
597 name = 'extdoctest' # call nosetests with --with-extdoctest
598 enabled = True
598 enabled = True
599
599
600 def __init__(self,exclude_patterns=None):
601 """Create a new ExtensionDoctest plugin.
602
603 Parameters
604 ----------
605
606 exclude_patterns : sequence of strings, optional
607 These patterns are compiled as regular expressions, subsequently used
608 to exclude any filename which matches them from inclusion in the test
609 suite (using pattern.search(), NOT pattern.match() ).
610 """
611
612 if exclude_patterns is None:
613 exclude_patterns = []
614 self.exclude_patterns = map(re.compile,exclude_patterns)
615 doctests.Doctest.__init__(self)
616
617 def options(self, parser, env=os.environ):
600 def options(self, parser, env=os.environ):
618 Plugin.options(self, parser, env)
601 Plugin.options(self, parser, env)
619 parser.add_option('--doctest-tests', action='store_true',
602 parser.add_option('--doctest-tests', action='store_true',
620 dest='doctest_tests',
603 dest='doctest_tests',
621 default=env.get('NOSE_DOCTEST_TESTS',True),
604 default=env.get('NOSE_DOCTEST_TESTS',True),
622 help="Also look for doctests in test modules. "
605 help="Also look for doctests in test modules. "
623 "Note that classes, methods and functions should "
606 "Note that classes, methods and functions should "
624 "have either doctests or non-doctest tests, "
607 "have either doctests or non-doctest tests, "
625 "not both. [NOSE_DOCTEST_TESTS]")
608 "not both. [NOSE_DOCTEST_TESTS]")
626 parser.add_option('--doctest-extension', action="append",
609 parser.add_option('--doctest-extension', action="append",
627 dest="doctestExtension",
610 dest="doctestExtension",
628 help="Also look for doctests in files with "
611 help="Also look for doctests in files with "
629 "this extension [NOSE_DOCTEST_EXTENSION]")
612 "this extension [NOSE_DOCTEST_EXTENSION]")
630 # Set the default as a list, if given in env; otherwise
613 # Set the default as a list, if given in env; otherwise
631 # an additional value set on the command line will cause
614 # an additional value set on the command line will cause
632 # an error.
615 # an error.
633 env_setting = env.get('NOSE_DOCTEST_EXTENSION')
616 env_setting = env.get('NOSE_DOCTEST_EXTENSION')
634 if env_setting is not None:
617 if env_setting is not None:
635 parser.set_defaults(doctestExtension=tolist(env_setting))
618 parser.set_defaults(doctestExtension=tolist(env_setting))
636
619
637
620
638 def configure(self, options, config):
621 def configure(self, options, config):
639 Plugin.configure(self, options, config)
622 Plugin.configure(self, options, config)
640 # Pull standard doctest plugin out of config; we will do doctesting
623 # Pull standard doctest plugin out of config; we will do doctesting
641 config.plugins.plugins = [p for p in config.plugins.plugins
624 config.plugins.plugins = [p for p in config.plugins.plugins
642 if p.name != 'doctest']
625 if p.name != 'doctest']
643 self.doctest_tests = options.doctest_tests
626 self.doctest_tests = options.doctest_tests
644 self.extension = tolist(options.doctestExtension)
627 self.extension = tolist(options.doctestExtension)
645
628
646 self.parser = doctest.DocTestParser()
629 self.parser = doctest.DocTestParser()
647 self.finder = DocTestFinder()
630 self.finder = DocTestFinder()
648 self.checker = IPDoctestOutputChecker()
631 self.checker = IPDoctestOutputChecker()
649 self.globs = None
632 self.globs = None
650 self.extraglobs = None
633 self.extraglobs = None
651
634
652
635
653 def loadTestsFromExtensionModule(self,filename):
636 def loadTestsFromExtensionModule(self,filename):
654 bpath,mod = os.path.split(filename)
637 bpath,mod = os.path.split(filename)
655 modname = os.path.splitext(mod)[0]
638 modname = os.path.splitext(mod)[0]
656 try:
639 try:
657 sys.path.append(bpath)
640 sys.path.append(bpath)
658 module = __import__(modname)
641 module = __import__(modname)
659 tests = list(self.loadTestsFromModule(module))
642 tests = list(self.loadTestsFromModule(module))
660 finally:
643 finally:
661 sys.path.pop()
644 sys.path.pop()
662 return tests
645 return tests
663
646
664 # NOTE: the method below is almost a copy of the original one in nose, with
647 # NOTE: the method below is almost a copy of the original one in nose, with
665 # a few modifications to control output checking.
648 # a few modifications to control output checking.
666
649
667 def loadTestsFromModule(self, module):
650 def loadTestsFromModule(self, module):
668 #print '*** ipdoctest - lTM',module # dbg
651 #print '*** ipdoctest - lTM',module # dbg
669
652
670 if not self.matches(module.__name__):
653 if not self.matches(module.__name__):
671 log.debug("Doctest doesn't want module %s", module)
654 log.debug("Doctest doesn't want module %s", module)
672 return
655 return
673
656
674 tests = self.finder.find(module,globs=self.globs,
657 tests = self.finder.find(module,globs=self.globs,
675 extraglobs=self.extraglobs)
658 extraglobs=self.extraglobs)
676 if not tests:
659 if not tests:
677 return
660 return
678
661
679 # always use whitespace and ellipsis options
662 # always use whitespace and ellipsis options
680 optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
663 optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
681
664
682 tests.sort()
665 tests.sort()
683 module_file = module.__file__
666 module_file = module.__file__
684 if module_file[-4:] in ('.pyc', '.pyo'):
667 if module_file[-4:] in ('.pyc', '.pyo'):
685 module_file = module_file[:-1]
668 module_file = module_file[:-1]
686 for test in tests:
669 for test in tests:
687 if not test.examples:
670 if not test.examples:
688 continue
671 continue
689 if not test.filename:
672 if not test.filename:
690 test.filename = module_file
673 test.filename = module_file
691
674
692 yield DocTestCase(test,
675 yield DocTestCase(test,
693 optionflags=optionflags,
676 optionflags=optionflags,
694 checker=self.checker)
677 checker=self.checker)
695
678
696
679
697 def loadTestsFromFile(self, filename):
680 def loadTestsFromFile(self, filename):
698 #print "ipdoctest - from file", filename # dbg
681 #print "ipdoctest - from file", filename # dbg
699 if is_extension_module(filename):
682 if is_extension_module(filename):
700 for t in self.loadTestsFromExtensionModule(filename):
683 for t in self.loadTestsFromExtensionModule(filename):
701 yield t
684 yield t
702 else:
685 else:
703 if self.extension and anyp(filename.endswith, self.extension):
686 if self.extension and anyp(filename.endswith, self.extension):
704 name = os.path.basename(filename)
687 name = os.path.basename(filename)
705 dh = open(filename)
688 dh = open(filename)
706 try:
689 try:
707 doc = dh.read()
690 doc = dh.read()
708 finally:
691 finally:
709 dh.close()
692 dh.close()
710 test = self.parser.get_doctest(
693 test = self.parser.get_doctest(
711 doc, globs={'__file__': filename}, name=name,
694 doc, globs={'__file__': filename}, name=name,
712 filename=filename, lineno=0)
695 filename=filename, lineno=0)
713 if test.examples:
696 if test.examples:
714 #print 'FileCase:',test.examples # dbg
697 #print 'FileCase:',test.examples # dbg
715 yield DocFileCase(test)
698 yield DocFileCase(test)
716 else:
699 else:
717 yield False # no tests to load
700 yield False # no tests to load
718
701
719 def wantFile(self,filename):
720 """Return whether the given filename should be scanned for tests.
721
722 Modified version that accepts extension modules as valid containers for
723 doctests.
724 """
725 #print '*** ipdoctest- wantFile:',filename # dbg
726
727 for pat in self.exclude_patterns:
728 if pat.search(filename):
729 # print '###>>> SKIP:',filename # dbg
730 return False
731
732 if is_extension_module(filename):
733 return True
734 else:
735 return doctests.Doctest.wantFile(self,filename)
736
737 def wantDirectory(self, directory):
738 """Return whether the given directory should be scanned for tests.
739
740 Modified version that supports exclusions.
741 """
742
743 for pat in self.exclude_patterns:
744 if pat.search(directory):
745 return False
746 return True
747
748
702
749 class IPythonDoctest(ExtensionDoctest):
703 class IPythonDoctest(ExtensionDoctest):
750 """Nose Plugin that supports doctests in extension modules.
704 """Nose Plugin that supports doctests in extension modules.
751 """
705 """
752 name = 'ipdoctest' # call nosetests with --with-ipdoctest
706 name = 'ipdoctest' # call nosetests with --with-ipdoctest
753 enabled = True
707 enabled = True
754
708
755 def makeTest(self, obj, parent):
709 def makeTest(self, obj, parent):
756 """Look for doctests in the given object, which will be a
710 """Look for doctests in the given object, which will be a
757 function, method or class.
711 function, method or class.
758 """
712 """
759 #print 'Plugin analyzing:', obj, parent # dbg
713 #print 'Plugin analyzing:', obj, parent # dbg
760 # always use whitespace and ellipsis options
714 # always use whitespace and ellipsis options
761 optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
715 optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
762
716
763 doctests = self.finder.find(obj, module=getmodule(parent))
717 doctests = self.finder.find(obj, module=getmodule(parent))
764 if doctests:
718 if doctests:
765 for test in doctests:
719 for test in doctests:
766 if len(test.examples) == 0:
720 if len(test.examples) == 0:
767 continue
721 continue
768
722
769 yield DocTestCase(test, obj=obj,
723 yield DocTestCase(test, obj=obj,
770 optionflags=optionflags,
724 optionflags=optionflags,
771 checker=self.checker)
725 checker=self.checker)
772
726
773 def options(self, parser, env=os.environ):
727 def options(self, parser, env=os.environ):
774 #print "Options for nose plugin:", self.name # dbg
728 #print "Options for nose plugin:", self.name # dbg
775 Plugin.options(self, parser, env)
729 Plugin.options(self, parser, env)
776 parser.add_option('--ipdoctest-tests', action='store_true',
730 parser.add_option('--ipdoctest-tests', action='store_true',
777 dest='ipdoctest_tests',
731 dest='ipdoctest_tests',
778 default=env.get('NOSE_IPDOCTEST_TESTS',True),
732 default=env.get('NOSE_IPDOCTEST_TESTS',True),
779 help="Also look for doctests in test modules. "
733 help="Also look for doctests in test modules. "
780 "Note that classes, methods and functions should "
734 "Note that classes, methods and functions should "
781 "have either doctests or non-doctest tests, "
735 "have either doctests or non-doctest tests, "
782 "not both. [NOSE_IPDOCTEST_TESTS]")
736 "not both. [NOSE_IPDOCTEST_TESTS]")
783 parser.add_option('--ipdoctest-extension', action="append",
737 parser.add_option('--ipdoctest-extension', action="append",
784 dest="ipdoctest_extension",
738 dest="ipdoctest_extension",
785 help="Also look for doctests in files with "
739 help="Also look for doctests in files with "
786 "this extension [NOSE_IPDOCTEST_EXTENSION]")
740 "this extension [NOSE_IPDOCTEST_EXTENSION]")
787 # Set the default as a list, if given in env; otherwise
741 # Set the default as a list, if given in env; otherwise
788 # an additional value set on the command line will cause
742 # an additional value set on the command line will cause
789 # an error.
743 # an error.
790 env_setting = env.get('NOSE_IPDOCTEST_EXTENSION')
744 env_setting = env.get('NOSE_IPDOCTEST_EXTENSION')
791 if env_setting is not None:
745 if env_setting is not None:
792 parser.set_defaults(ipdoctest_extension=tolist(env_setting))
746 parser.set_defaults(ipdoctest_extension=tolist(env_setting))
793
747
794 def configure(self, options, config):
748 def configure(self, options, config):
795 #print "Configuring nose plugin:", self.name # dbg
749 #print "Configuring nose plugin:", self.name # dbg
796 Plugin.configure(self, options, config)
750 Plugin.configure(self, options, config)
797 # Pull standard doctest plugin out of config; we will do doctesting
751 # Pull standard doctest plugin out of config; we will do doctesting
798 config.plugins.plugins = [p for p in config.plugins.plugins
752 config.plugins.plugins = [p for p in config.plugins.plugins
799 if p.name != 'doctest']
753 if p.name != 'doctest']
800 self.doctest_tests = options.ipdoctest_tests
754 self.doctest_tests = options.ipdoctest_tests
801 self.extension = tolist(options.ipdoctest_extension)
755 self.extension = tolist(options.ipdoctest_extension)
802
756
803 self.parser = IPDocTestParser()
757 self.parser = IPDocTestParser()
804 self.finder = DocTestFinder(parser=self.parser)
758 self.finder = DocTestFinder(parser=self.parser)
805 self.checker = IPDoctestOutputChecker()
759 self.checker = IPDoctestOutputChecker()
806 self.globs = None
760 self.globs = None
807 self.extraglobs = None
761 self.extraglobs = None
General Comments 0
You need to be logged in to leave comments. Login now