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