##// END OF EJS Templates
Start refactoring test machinery
Thomas Kluyver -
Show More
@@ -30,6 +30,7 b' from __future__ import print_function'
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
@@ -38,6 +39,7 b' 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
@@ -91,7 +93,7 b' def monkeypatch_xunit():'
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__
@@ -155,144 +157,203 b' min_zmq = (2,1,11)'
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 ]
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():
163 """Make patterns of modules and packages to exclude from testing.
301 #-----------------------------------------------------------------------------
302 # Functions and classes
303 #-----------------------------------------------------------------------------
164 304
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.
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)
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
172 # these below
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'))
317 name = 'exclusions'
318 score = 3000 # Should come before any other plugins
201 319
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'))
206
207 # We do this unconditionally, so that the test suite doesn't import
208 # 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'))
217
218 # These have to be skipped on win32 because the use echo, rm, cd, etc.
219 # See ticket https://github.com/ipython/ipython/issues/87
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]
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)
280 338
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)
288 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
289 warn("Excluding nonexistent file: %r" % exclusion)
290
291 return exclusions
292
293 special_test_suites = {
294 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
295 }
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():
@@ -309,11 +370,8 b' def run_iptest():'
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
@@ -344,8 +402,11 b' def run_iptest():'
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
@@ -354,7 +415,7 b' def run_iptest():'
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
@@ -18,93 +18,67 b' test suite.'
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)
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])
82 72
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:
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
73
74 def launch(self):
75 # print('*** ENV:', self.env) # dbg
76 # print('*** CMD:', self.cmd) # dbg
77 env = os.environ.copy()
78 env.update(self.env)
79 output = subprocess.PIPE if self.buffer_output else None
80 self.process = subprocess.Popen(self.cmd, stdout=output,
81 stderr=output, env=env)
108 82
109 83 def run(self):
110 84 """Run the stored commands"""
@@ -121,71 +95,61 b' class IPTester(object):'
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
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 """
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.
151 103
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')
104 try:
105 print('Cleaning up stale PID: %d' % subp.pid)
106 subp.kill()
107 except: # (OSError, WindowsError) ?
108 # This is just a best effort, if we fail or the process was
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']:
160 nose_pkg_names.append('html')
118 if subp.poll() is None:
119 # The process did not die...
120 print('... failed. Manual cleanup may be required.')
161 121
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')
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 ]
122 for td in self.dirs:
123 td.cleanup()
179 124
180 for name in special_test_suites:
181 runners.append((name, IPTester('iptest', params=name)))
182
183 return runners
184
185 def do_run(x):
186 print('IPython test group:',x[0])
187 ret = x[1].run()
188 return ret
125 __del__ = cleanup
126
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
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 154 def report():
191 155 """Return a string with a summary report of test-related variables."""
@@ -213,7 +177,7 b' def report():'
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
@@ -232,43 +196,31 b' def run_iptestall(inc_slow=False, fast=False):'
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 print('*'*70)
257 for ((name, runner), res) in zip(runners, all_res):
258 tgroup = 'IPython test group: ' + name
259 res_string = 'OK' if res == 0 else 'FAILED'
260 res_string = res_string.rjust(70 - len(tgroup), '.')
261 print(tgroup + res_string)
262 if res:
263 failed.append( (name, runner) )
264 if res == -signal.SIGINT:
265 print("Interrupted")
266 break
267 finally:
268 os.chdir(curdir)
209 print('*'*70)
210 for (controller, res) in pool.imap_unordered(do_run, controllers):
211 tgroup = 'IPython test group: ' + controller.section
212 res_string = 'OK' if res == 0 else 'FAILED'
213 res_string = res_string.rjust(70 - len(tgroup), '.')
214 print(tgroup + res_string)
215 if res:
216 failed.append(controller)
217 if res == -signal.SIGINT:
218 print("Interrupted")
219 break
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()
@@ -284,11 +236,11 b' def run_iptestall(inc_slow=False, fast=False):'
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
@@ -296,23 +248,27 b' def run_iptestall(inc_slow=False, fast=False):'
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:
301 from .iptest import run_iptest
302 # This is in-process
303 run_iptest()
304 else:
305 inc_slow = "--all" in sys.argv
306 if inc_slow:
307 sys.argv.remove("--all")
308
309 fast = "--fast" in sys.argv
310 if fast:
311 sys.argv.remove("--fast")
312 IPTester.buffer_output = True
313
314 # This starts subprocesses
315 run_iptestall(inc_slow=inc_slow, fast=fast)
251 if len(sys.argv) > 1 and (sys.argv[1] in test_sections):
252 from .iptest import run_iptest
253 # This is in-process
254 run_iptest()
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.')
266
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 274 if __name__ == '__main__':
@@ -597,23 +597,6 b' class ExtensionDoctest(doctests.Doctest):'
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',
@@ -716,35 +699,6 b' class ExtensionDoctest(doctests.Doctest):'
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.
General Comments 0
You need to be logged in to leave comments. Login now