##// END OF EJS Templates
Start refactoring test machinery
Thomas Kluyver -
Show More
@@ -30,6 +30,7 b' from __future__ import print_function'
30 import glob
30 import glob
31 import os
31 import os
32 import os.path as path
32 import os.path as path
33 import re
33 import sys
34 import sys
34 import warnings
35 import warnings
35
36
@@ -38,6 +39,7 b' import nose.plugins.builtin'
38 from nose.plugins.xunit import Xunit
39 from nose.plugins.xunit import Xunit
39 from nose import SkipTest
40 from nose import SkipTest
40 from nose.core import TestProgram
41 from nose.core import TestProgram
42 from nose.plugins import Plugin
41
43
42 # Our own imports
44 # Our own imports
43 from IPython.utils.importstring import import_item
45 from IPython.utils.importstring import import_item
@@ -91,7 +93,7 b' def monkeypatch_xunit():'
91 Xunit.addError = addError
93 Xunit.addError = addError
92
94
93 #-----------------------------------------------------------------------------
95 #-----------------------------------------------------------------------------
94 # Logic for skipping doctests
96 # Check which dependencies are installed and greater than minimum version.
95 #-----------------------------------------------------------------------------
97 #-----------------------------------------------------------------------------
96 def extract_version(mod):
98 def extract_version(mod):
97 return mod.__version__
99 return mod.__version__
@@ -155,144 +157,203 b' min_zmq = (2,1,11)'
155 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
157 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
156
158
157 #-----------------------------------------------------------------------------
159 #-----------------------------------------------------------------------------
158 # Functions and classes
160 # Test suite definitions
159 #-----------------------------------------------------------------------------
161 #-----------------------------------------------------------------------------
160
162
163 test_group_names = ['parallel', 'kernel', 'kernel.inprocess', 'config', 'core',
164 'extensions', 'lib', 'terminal', 'testing', 'utils',
165 'nbformat', 'qt', 'html', 'nbconvert'
166 ]
167
168 class TestSection(object):
169 def __init__(self, name, includes):
170 self.name = name
171 self.includes = includes
172 self.excludes = []
173 self.dependencies = []
174 self.enabled = True
175
176 def exclude(self, module):
177 if not module.startswith('IPython'):
178 module = self.includes[0] + "." + module
179 self.excludes.append(module.replace('.', os.sep))
180
181 def requires(self, *packages):
182 self.dependencies.extend(packages)
183
184 @property
185 def will_run(self):
186 return self.enabled and all(have[p] for p in self.dependencies)
187
188 # Name -> (include, exclude, dependencies_met)
189 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
190
191 # Exclusions and dependencies
192 # ---------------------------
193
194 # core:
195 sec = test_sections['core']
196 if not have['sqlite3']:
197 sec.exclude('tests.test_history')
198 sec.exclude('history')
199 if not have['matplotlib']:
200 sec.exclude('pylabtools'),
201 sec.exclude('tests.test_pylabtools')
202
203 # lib:
204 sec = test_sections['lib']
205 if not have['wx']:
206 sec.exclude('inputhookwx')
207 if not have['pexpect']:
208 sec.exclude('irunner')
209 sec.exclude('tests.test_irunner')
210 if not have['zmq']:
211 sec.exclude('kernel')
212 # We do this unconditionally, so that the test suite doesn't import
213 # gtk, changing the default encoding and masking some unicode bugs.
214 sec.exclude('inputhookgtk')
215 # Testing inputhook will need a lot of thought, to figure out
216 # how to have tests that don't lock up with the gui event
217 # loops in the picture
218 sec.exclude('inputhook')
219
220 # testing:
221 sec = test_sections['lib']
222 # This guy is probably attic material
223 sec.exclude('mkdoctests')
224 # These have to be skipped on win32 because the use echo, rm, cd, etc.
225 # See ticket https://github.com/ipython/ipython/issues/87
226 if sys.platform == 'win32':
227 sec.exclude('plugin.test_exampleip')
228 sec.exclude('plugin.dtexample')
229
230 # terminal:
231 if (not have['pexpect']) or (not have['zmq']):
232 test_sections['terminal'].exclude('console')
233
234 # parallel
235 sec = test_sections['parallel']
236 sec.requires('zmq')
237 if not have['pymongo']:
238 sec.exclude('controller.mongodb')
239 sec.exclude('tests.test_mongodb')
240
241 # kernel:
242 sec = test_sections['kernel']
243 sec.requires('zmq')
244 # The in-process kernel tests are done in a separate section
245 sec.exclude('inprocess')
246 # importing gtk sets the default encoding, which we want to avoid
247 sec.exclude('zmq.gui.gtkembed')
248 if not have['matplotlib']:
249 sec.exclude('zmq.pylab')
250
251 # kernel.inprocess:
252 test_sections['kernel.inprocess'].requires('zmq')
253
254 # extensions:
255 sec = test_sections['extensions']
256 if not have['cython']:
257 sec.exclude('cythonmagic')
258 sec.exclude('tests.test_cythonmagic')
259 if not have['oct2py']:
260 sec.exclude('octavemagic')
261 sec.exclude('tests.test_octavemagic')
262 if not have['rpy2'] or not have['numpy']:
263 sec.exclude('rmagic')
264 sec.exclude('tests.test_rmagic')
265 # autoreload does some strange stuff, so move it to its own test section
266 sec.exclude('autoreload')
267 sec.exclude('tests.test_autoreload')
268 test_sections['autoreload'] = TestSection('autoreload',
269 ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
270 test_group_names.append('autoreload')
271
272 # qt:
273 test_sections['qt'].requires('zmq', 'qt', 'pygments')
274
275 # html:
276 sec = test_sections['html']
277 sec.requires('zmq', 'tornado')
278 # The notebook 'static' directory contains JS, css and other
279 # files for web serving. Occasionally projects may put a .py
280 # file in there (MathJax ships a conf.py), so we might as
281 # well play it safe and skip the whole thing.
282 sec.exclude('static')
283 sec.exclude('fabfile')
284 if not have['jinja2']:
285 sec.exclude('notebookapp')
286 if not have['azure']:
287 sec.exclude('services.notebooks.azurenbmanager')
288
289 # config:
290 # Config files aren't really importable stand-alone
291 test_sections['config'].exclude('profile')
292
293 # nbconvert:
294 sec = test_sections['nbconvert']
295 sec.requires('pygments', 'jinja2', 'sphinx')
296 # Exclude nbconvert directories containing config files used to test.
297 # Executing the config files with iptest would cause an exception.
298 sec.exclude('tests.files')
299 sec.exclude('exporters.tests.files')
161
300
162 def make_exclude():
301 #-----------------------------------------------------------------------------
163 """Make patterns of modules and packages to exclude from testing.
302 # Functions and classes
303 #-----------------------------------------------------------------------------
164
304
165 For the IPythonDoctest plugin, we need to exclude certain patterns that
305 def check_exclusions_exist():
166 cause testing problems. We should strive to minimize the number of
306 parent = os.path.dirname(get_ipython_package_dir())
167 skipped modules, since this means untested code.
307 for sec in test_sections:
308 for pattern in sec.exclusions:
309 fullpath = pjoin(parent, pattern)
310 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
311 warn("Excluding nonexistent file: %r" % pattern)
168
312
169 These modules and packages will NOT get scanned by nose at all for tests.
313
314 class ExclusionPlugin(Plugin):
315 """A nose plugin to effect our exclusions of files and directories.
170 """
316 """
171 # Simple utility to make IPython paths more readably, we need a lot of
317 name = 'exclusions'
172 # these below
318 score = 3000 # Should come before any other plugins
173 ipjoin = lambda *paths: pjoin('IPython', *paths)
174
175 exclusions = [ipjoin('external'),
176 ipjoin('quarantine'),
177 ipjoin('deathrow'),
178 # This guy is probably attic material
179 ipjoin('testing', 'mkdoctests'),
180 # Testing inputhook will need a lot of thought, to figure out
181 # how to have tests that don't lock up with the gui event
182 # loops in the picture
183 ipjoin('lib', 'inputhook'),
184 # Config files aren't really importable stand-alone
185 ipjoin('config', 'profile'),
186 # The notebook 'static' directory contains JS, css and other
187 # files for web serving. Occasionally projects may put a .py
188 # file in there (MathJax ships a conf.py), so we might as
189 # well play it safe and skip the whole thing.
190 ipjoin('html', 'static'),
191 ipjoin('html', 'fabfile'),
192 ]
193 if not have['sqlite3']:
194 exclusions.append(ipjoin('core', 'tests', 'test_history'))
195 exclusions.append(ipjoin('core', 'history'))
196 if not have['wx']:
197 exclusions.append(ipjoin('lib', 'inputhookwx'))
198
199 if 'IPython.kernel.inprocess' not in sys.argv:
200 exclusions.append(ipjoin('kernel', 'inprocess'))
201
319
202 # FIXME: temporarily disable autoreload tests, as they can produce
320 def __init__(self, exclude_patterns=None):
203 # spurious failures in subsequent tests (cythonmagic).
321 """
204 exclusions.append(ipjoin('extensions', 'autoreload'))
322 Parameters
205 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
323 ----------
206
324
207 # We do this unconditionally, so that the test suite doesn't import
325 exclude_patterns : sequence of strings, optional
208 # gtk, changing the default encoding and masking some unicode bugs.
326 These patterns are compiled as regular expressions, subsequently used
209 exclusions.append(ipjoin('lib', 'inputhookgtk'))
327 to exclude any filename which matches them from inclusion in the test
210 exclusions.append(ipjoin('kernel', 'zmq', 'gui', 'gtkembed'))
328 suite (using pattern.search(), NOT pattern.match() ).
211
329 """
212 #Also done unconditionally, exclude nbconvert directories containing
330
213 #config files used to test. Executing the config files with iptest would
331 if exclude_patterns is None:
214 #cause an exception.
332 exclude_patterns = []
215 exclusions.append(ipjoin('nbconvert', 'tests', 'files'))
333 self.exclude_patterns = [re.compile(p) for p in exclude_patterns]
216 exclusions.append(ipjoin('nbconvert', 'exporters', 'tests', 'files'))
334 super(ExclusionPlugin, self).__init__()
217
335
218 # These have to be skipped on win32 because the use echo, rm, cd, etc.
336 def options(self, parser, env=os.environ):
219 # See ticket https://github.com/ipython/ipython/issues/87
337 Plugin.options(self, parser, env)
220 if sys.platform == 'win32':
221 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
222 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
223
224 if not have['pexpect']:
225 exclusions.extend([ipjoin('lib', 'irunner'),
226 ipjoin('lib', 'tests', 'test_irunner'),
227 ipjoin('terminal', 'console'),
228 ])
229
230 if not have['zmq']:
231 exclusions.append(ipjoin('lib', 'kernel'))
232 exclusions.append(ipjoin('kernel'))
233 exclusions.append(ipjoin('qt'))
234 exclusions.append(ipjoin('html'))
235 exclusions.append(ipjoin('consoleapp.py'))
236 exclusions.append(ipjoin('terminal', 'console'))
237 exclusions.append(ipjoin('parallel'))
238 elif not have['qt'] or not have['pygments']:
239 exclusions.append(ipjoin('qt'))
240
241 if not have['pymongo']:
242 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
243 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
244
245 if not have['matplotlib']:
246 exclusions.extend([ipjoin('core', 'pylabtools'),
247 ipjoin('core', 'tests', 'test_pylabtools'),
248 ipjoin('kernel', 'zmq', 'pylab'),
249 ])
250
251 if not have['cython']:
252 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
253 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
254
255 if not have['oct2py']:
256 exclusions.extend([ipjoin('extensions', 'octavemagic')])
257 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
258
259 if not have['tornado']:
260 exclusions.append(ipjoin('html'))
261 exclusions.append(ipjoin('nbconvert', 'post_processors', 'serve'))
262 exclusions.append(ipjoin('nbconvert', 'post_processors', 'tests', 'test_serve'))
263
264 if not have['jinja2']:
265 exclusions.append(ipjoin('html', 'notebookapp'))
266
267 if not have['rpy2'] or not have['numpy']:
268 exclusions.append(ipjoin('extensions', 'rmagic'))
269 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
270
271 if not have['azure']:
272 exclusions.append(ipjoin('html', 'services', 'notebooks', 'azurenbmanager'))
273
274 if not all((have['pygments'], have['jinja2'], have['sphinx'])):
275 exclusions.append(ipjoin('nbconvert'))
276
277 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
278 if sys.platform == 'win32':
279 exclusions = [s.replace('\\','\\\\') for s in exclusions]
280
338
281 # check for any exclusions that don't seem to exist:
339 def configure(self, options, config):
282 parent, _ = os.path.split(get_ipython_package_dir())
340 Plugin.configure(self, options, config)
283 for exclusion in exclusions:
341 # Override nose trying to disable plugin.
284 if exclusion.endswith(('deathrow', 'quarantine')):
342 self.enabled = True
285 # ignore deathrow/quarantine, which exist in dev, but not install
343
286 continue
344 def wantFile(self, filename):
287 fullpath = pjoin(parent, exclusion)
345 """Return whether the given filename should be scanned for tests.
288 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
346 """
289 warn("Excluding nonexistent file: %r" % exclusion)
347 if any(pat.search(filename) for pat in self.exclude_patterns):
290
348 return False
291 return exclusions
349 return None
292
350
293 special_test_suites = {
351 def wantDirectory(self, directory):
294 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
352 """Return whether the given directory should be scanned for tests.
295 }
353 """
354 if any(pat.search(directory) for pat in self.exclude_patterns):
355 return False
356 return None
296
357
297
358
298 def run_iptest():
359 def run_iptest():
@@ -309,11 +370,8 b' def run_iptest():'
309 warnings.filterwarnings('ignore',
370 warnings.filterwarnings('ignore',
310 'This will be removed soon. Use IPython.testing.util instead')
371 'This will be removed soon. Use IPython.testing.util instead')
311
372
312 if sys.argv[1] in special_test_suites:
373 section = test_sections[sys.argv[1]]
313 sys.argv[1:2] = special_test_suites[sys.argv[1]]
374 sys.argv[1:2] = section.includes
314 special_suite = True
315 else:
316 special_suite = False
317
375
318 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
376 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
319
377
@@ -344,8 +402,11 b' def run_iptest():'
344
402
345 # use our plugin for doctesting. It will remove the standard doctest plugin
403 # use our plugin for doctesting. It will remove the standard doctest plugin
346 # if it finds it enabled
404 # if it finds it enabled
347 ipdt = IPythonDoctest() if special_suite else IPythonDoctest(make_exclude())
405 plugins = [ExclusionPlugin(section.excludes), IPythonDoctest(), KnownFailure()]
348 plugins = [ipdt, KnownFailure()]
406
407 # Use working directory set by parent process (see iptestcontroller)
408 if 'IPTEST_WORKING_DIR' in os.environ:
409 os.chdir(os.environ['IPTEST_WORKING_DIR'])
349
410
350 # We need a global ipython running in this process, but the special
411 # We need a global ipython running in this process, but the special
351 # in-process group spawns its own IPython kernels, so for *that* group we
412 # in-process group spawns its own IPython kernels, so for *that* group we
@@ -354,7 +415,7 b' def run_iptest():'
354 # assumptions about what needs to be a singleton and what doesn't (app
415 # assumptions about what needs to be a singleton and what doesn't (app
355 # objects should, individual shells shouldn't). But for now, this
416 # objects should, individual shells shouldn't). But for now, this
356 # workaround allows the test suite for the inprocess module to complete.
417 # workaround allows the test suite for the inprocess module to complete.
357 if not 'IPython.kernel.inprocess' in sys.argv:
418 if section.name != 'kernel.inprocess':
358 globalipapp.start_ipython()
419 globalipapp.start_ipython()
359
420
360 # Now nose can run
421 # Now nose can run
@@ -18,93 +18,67 b' test suite.'
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
19 from __future__ import print_function
20
20
21 import argparse
21 import multiprocessing.pool
22 import multiprocessing.pool
22 import os
23 import os
23 import signal
24 import signal
24 import sys
25 import sys
25 import subprocess
26 import subprocess
26 import tempfile
27 import time
27 import time
28
28
29 from .iptest import have, special_test_suites
29 from .iptest import have, test_group_names, test_sections
30 from IPython.utils import py3compat
30 from IPython.utils import py3compat
31 from IPython.utils.path import get_ipython_module_path
32 from IPython.utils.process import pycmd2argv
33 from IPython.utils.sysinfo import sys_info
31 from IPython.utils.sysinfo import sys_info
34 from IPython.utils.tempdir import TemporaryDirectory
32 from IPython.utils.tempdir import TemporaryDirectory
35
33
36
34
37 class IPTester(object):
35 class IPTestController(object):
38 """Call that calls iptest or trial in a subprocess.
36 """Run iptest in a subprocess
39 """
37 """
40 #: string, name of test runner that will be called
38 #: str, IPython test suite to be executed.
41 runner = None
39 section = None
42 #: list, parameters for test runner
40 #: list, command line arguments to be executed
43 params = None
41 cmd = None
44 #: list, arguments of system call to be made to call test runner
42 #: dict, extra environment variables to set for the subprocess
45 call_args = None
43 env = None
46 #: list, subprocesses we start (for cleanup)
44 #: list, TemporaryDirectory instances to clear up when the process finishes
47 processes = None
45 dirs = None
48 #: str, coverage xml output file
46 #: subprocess.Popen instance
49 coverage_xml = None
47 process = None
50 buffer_output = False
48 buffer_output = False
51
49
52 def __init__(self, runner='iptest', params=None):
50 def __init__(self, section):
53 """Create new test runner."""
51 """Create new test runner."""
54 if runner == 'iptest':
52 self.section = section
55 iptest_app = os.path.abspath(get_ipython_module_path('IPython.testing.iptest'))
53 self.cmd = [sys.executable, '-m', 'IPython.testing.iptest', section]
56 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
54 self.env = {}
57 else:
55 self.dirs = []
58 raise Exception('Not a valid test runner: %s' % repr(runner))
56 ipydir = TemporaryDirectory()
59 if params is None:
57 self.dirs.append(ipydir)
60 params = []
58 self.env['IPYTHONDIR'] = ipydir.name
61 if isinstance(params, str):
59 workingdir = TemporaryDirectory()
62 params = [params]
60 self.dirs.append(workingdir)
63 self.params = params
61 self.env['IPTEST_WORKING_DIR'] = workingdir.name
64
62
65 # Assemble call
63 def add_xunit(self):
66 self.call_args = self.runner+self.params
64 xunit_file = os.path.abspath(self.section + '.xunit.xml')
67
65 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
68 # Find the section we're testing (IPython.foo)
66
69 for sect in self.params:
67 def add_coverage(self, xml=True):
70 if sect.startswith('IPython') or sect in special_test_suites: break
68 self.cmd.extend(['--with-coverage', '--cover-package', self.section])
71 else:
69 if xml:
72 raise ValueError("Section not found", self.params)
70 coverage_xml = os.path.abspath(self.section + ".coverage.xml")
73
71 self.cmd.extend(['--cover-xml', '--cover-xml-file', coverage_xml])
74 if '--with-xunit' in self.call_args:
75
76 self.call_args.append('--xunit-file')
77 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
78 xunit_file = os.path.abspath(sect+'.xunit.xml')
79 if sys.platform == 'win32':
80 xunit_file = '"%s"' % xunit_file
81 self.call_args.append(xunit_file)
82
72
83 if '--with-xml-coverage' in self.call_args:
73
84 self.coverage_xml = os.path.abspath(sect+".coverage.xml")
74 def launch(self):
85 self.call_args.remove('--with-xml-coverage')
75 # print('*** ENV:', self.env) # dbg
86 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
76 # print('*** CMD:', self.cmd) # dbg
87
77 env = os.environ.copy()
88 # Store anything we start to clean up on deletion
78 env.update(self.env)
89 self.processes = []
79 output = subprocess.PIPE if self.buffer_output else None
90
80 self.process = subprocess.Popen(self.cmd, stdout=output,
91 def _run_cmd(self):
81 stderr=output, env=env)
92 with TemporaryDirectory() as IPYTHONDIR:
93 env = os.environ.copy()
94 env['IPYTHONDIR'] = IPYTHONDIR
95 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
96 output = subprocess.PIPE if self.buffer_output else None
97 subp = subprocess.Popen(self.call_args, stdout=output,
98 stderr=output, env=env)
99 self.processes.append(subp)
100 # If this fails, the process will be left in self.processes and
101 # cleaned up later, but if the wait call succeeds, then we can
102 # clear the stored process.
103 retcode = subp.wait()
104 self.processes.pop()
105 self.stdout = subp.stdout
106 self.stderr = subp.stderr
107 return retcode
108
82
109 def run(self):
83 def run(self):
110 """Run the stored commands"""
84 """Run the stored commands"""
@@ -121,71 +95,61 b' class IPTester(object):'
121 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
95 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
122 return retcode
96 return retcode
123
97
124 def __del__(self):
98 def cleanup(self):
125 """Cleanup on exit by killing any leftover processes."""
99 """Cleanup on exit by killing any leftover processes."""
126 for subp in self.processes:
100 subp = self.process
127 if subp.poll() is not None:
101 if subp is None or (subp.poll() is not None):
128 continue # process is already dead
102 return # Process doesn't exist, or is already dead.
129
130 try:
131 print('Cleaning up stale PID: %d' % subp.pid)
132 subp.kill()
133 except: # (OSError, WindowsError) ?
134 # This is just a best effort, if we fail or the process was
135 # really gone, ignore it.
136 pass
137 else:
138 for i in range(10):
139 if subp.poll() is None:
140 time.sleep(0.1)
141 else:
142 break
143
144 if subp.poll() is None:
145 # The process did not die...
146 print('... failed. Manual cleanup may be required.')
147
148 def make_runners(inc_slow=False):
149 """Define the top-level packages that need to be tested.
150 """
151
103
152 # Packages to be tested via nose, that only depend on the stdlib
104 try:
153 nose_pkg_names = ['config', 'core', 'extensions', 'lib', 'terminal',
105 print('Cleaning up stale PID: %d' % subp.pid)
154 'testing', 'utils', 'nbformat']
106 subp.kill()
155
107 except: # (OSError, WindowsError) ?
156 if have['qt']:
108 # This is just a best effort, if we fail or the process was
157 nose_pkg_names.append('qt')
109 # really gone, ignore it.
110 pass
111 else:
112 for i in range(10):
113 if subp.poll() is None:
114 time.sleep(0.1)
115 else:
116 break
158
117
159 if have['tornado']:
118 if subp.poll() is None:
160 nose_pkg_names.append('html')
119 # The process did not die...
120 print('... failed. Manual cleanup may be required.')
161
121
162 if have['zmq']:
122 for td in self.dirs:
163 nose_pkg_names.insert(0, 'kernel')
123 td.cleanup()
164 nose_pkg_names.insert(1, 'kernel.inprocess')
165 if inc_slow:
166 nose_pkg_names.insert(0, 'parallel')
167
168 if all((have['pygments'], have['jinja2'], have['sphinx'])):
169 nose_pkg_names.append('nbconvert')
170
171 # For debugging this code, only load quick stuff
172 #nose_pkg_names = ['core', 'extensions'] # dbg
173
174 # Make fully qualified package names prepending 'IPython.' to our name lists
175 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
176
177 # Make runners
178 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
179
124
180 for name in special_test_suites:
125 __del__ = cleanup
181 runners.append((name, IPTester('iptest', params=name)))
126
182
127 def test_controllers_to_run(inc_slow=False):
183 return runners
128 """Returns an ordered list of IPTestController instances to be run."""
184
129 res = []
185 def do_run(x):
130 if not inc_slow:
186 print('IPython test group:',x[0])
131 test_sections['parallel'].enabled = False
187 ret = x[1].run()
132 for name in test_group_names:
188 return ret
133 if test_sections[name].will_run:
134 res.append(IPTestController(name))
135 return res
136
137 def do_run(controller):
138 try:
139 try:
140 controller.launch()
141 except Exception:
142 import traceback
143 traceback.print_exc()
144 return controller, 1 # signal failure
145
146 exitcode = controller.process.wait()
147 controller.cleanup()
148 return controller, exitcode
149
150 except KeyboardInterrupt:
151 controller.cleanup()
152 return controller, -signal.SIGINT
189
153
190 def report():
154 def report():
191 """Return a string with a summary report of test-related variables."""
155 """Return a string with a summary report of test-related variables."""
@@ -213,7 +177,7 b' def report():'
213
177
214 return ''.join(out)
178 return ''.join(out)
215
179
216 def run_iptestall(inc_slow=False, fast=False):
180 def run_iptestall(inc_slow=False, jobs=1, xunit=False, coverage=False):
217 """Run the entire IPython test suite by calling nose and trial.
181 """Run the entire IPython test suite by calling nose and trial.
218
182
219 This function constructs :class:`IPTester` instances for all IPython
183 This function constructs :class:`IPTester` instances for all IPython
@@ -232,43 +196,31 b' def run_iptestall(inc_slow=False, fast=False):'
232 Run the test suite in parallel, if True, using as many threads as there
196 Run the test suite in parallel, if True, using as many threads as there
233 are processors
197 are processors
234 """
198 """
235 if fast:
199 pool = multiprocessing.pool.ThreadPool(jobs)
236 p = multiprocessing.pool.ThreadPool()
200 if jobs != 1:
237 else:
201 IPTestController.buffer_output = True
238 p = multiprocessing.pool.ThreadPool(1)
239
240 runners = make_runners(inc_slow=inc_slow)
241
202
242 # Run the test runners in a temporary dir so we can nuke it when finished
203 controllers = test_controllers_to_run(inc_slow=inc_slow)
243 # to clean up any junk files left over by accident. This also makes it
244 # robust against being run in non-writeable directories by mistake, as the
245 # temp dir will always be user-writeable.
246 curdir = os.getcwdu()
247 testdir = tempfile.gettempdir()
248 os.chdir(testdir)
249
204
250 # Run all test runners, tracking execution time
205 # Run all test runners, tracking execution time
251 failed = []
206 failed = []
252 t_start = time.time()
207 t_start = time.time()
253
208
254 try:
209 print('*'*70)
255 all_res = p.map(do_run, runners)
210 for (controller, res) in pool.imap_unordered(do_run, controllers):
256 print('*'*70)
211 tgroup = 'IPython test group: ' + controller.section
257 for ((name, runner), res) in zip(runners, all_res):
212 res_string = 'OK' if res == 0 else 'FAILED'
258 tgroup = 'IPython test group: ' + name
213 res_string = res_string.rjust(70 - len(tgroup), '.')
259 res_string = 'OK' if res == 0 else 'FAILED'
214 print(tgroup + res_string)
260 res_string = res_string.rjust(70 - len(tgroup), '.')
215 if res:
261 print(tgroup + res_string)
216 failed.append(controller)
262 if res:
217 if res == -signal.SIGINT:
263 failed.append( (name, runner) )
218 print("Interrupted")
264 if res == -signal.SIGINT:
219 break
265 print("Interrupted")
220
266 break
267 finally:
268 os.chdir(curdir)
269 t_end = time.time()
221 t_end = time.time()
270 t_tests = t_end - t_start
222 t_tests = t_end - t_start
271 nrunners = len(runners)
223 nrunners = len(controllers)
272 nfail = len(failed)
224 nfail = len(failed)
273 # summarize results
225 # summarize results
274 print()
226 print()
@@ -284,11 +236,11 b' def run_iptestall(inc_slow=False, fast=False):'
284 # If anything went wrong, point out what command to rerun manually to
236 # If anything went wrong, point out what command to rerun manually to
285 # see the actual errors and individual summary
237 # see the actual errors and individual summary
286 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
238 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
287 for name, failed_runner in failed:
239 for controller in failed:
288 print('-'*40)
240 print('-'*40)
289 print('Runner failed:',name)
241 print('Runner failed:', controller.section)
290 print('You may wish to rerun this one individually, with:')
242 print('You may wish to rerun this one individually, with:')
291 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
243 failed_call_args = [py3compat.cast_unicode(x) for x in controller.cmd]
292 print(u' '.join(failed_call_args))
244 print(u' '.join(failed_call_args))
293 print()
245 print()
294 # Ensure that our exit code indicates failure
246 # Ensure that our exit code indicates failure
@@ -296,23 +248,27 b' def run_iptestall(inc_slow=False, fast=False):'
296
248
297
249
298 def main():
250 def main():
299 for arg in sys.argv[1:]:
251 if len(sys.argv) > 1 and (sys.argv[1] in test_sections):
300 if arg.startswith('IPython') or arg in special_test_suites:
252 from .iptest import run_iptest
301 from .iptest import run_iptest
253 # This is in-process
302 # This is in-process
254 run_iptest()
303 run_iptest()
255 return
304 else:
256
305 inc_slow = "--all" in sys.argv
257 parser = argparse.ArgumentParser(description='Run IPython test suite')
306 if inc_slow:
258 parser.add_argument('--all', action='store_true',
307 sys.argv.remove("--all")
259 help='Include slow tests not run by default.')
308
260 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
309 fast = "--fast" in sys.argv
261 help='Run test sections in parallel.')
310 if fast:
262 parser.add_argument('--xunit', action='store_true',
311 sys.argv.remove("--fast")
263 help='Produce Xunit XML results')
312 IPTester.buffer_output = True
264 parser.add_argument('--coverage', action='store_true',
313
265 help='Measure test coverage.')
314 # This starts subprocesses
266
315 run_iptestall(inc_slow=inc_slow, fast=fast)
267 options = parser.parse_args()
268
269 # This starts subprocesses
270 run_iptestall(inc_slow=options.all, jobs=options.fast,
271 xunit=options.xunit, coverage=options.coverage)
316
272
317
273
318 if __name__ == '__main__':
274 if __name__ == '__main__':
@@ -597,23 +597,6 b' class ExtensionDoctest(doctests.Doctest):'
597 name = 'extdoctest' # call nosetests with --with-extdoctest
597 name = 'extdoctest' # call nosetests with --with-extdoctest
598 enabled = True
598 enabled = True
599
599
600 def __init__(self,exclude_patterns=None):
601 """Create a new ExtensionDoctest plugin.
602
603 Parameters
604 ----------
605
606 exclude_patterns : sequence of strings, optional
607 These patterns are compiled as regular expressions, subsequently used
608 to exclude any filename which matches them from inclusion in the test
609 suite (using pattern.search(), NOT pattern.match() ).
610 """
611
612 if exclude_patterns is None:
613 exclude_patterns = []
614 self.exclude_patterns = map(re.compile,exclude_patterns)
615 doctests.Doctest.__init__(self)
616
617 def options(self, parser, env=os.environ):
600 def options(self, parser, env=os.environ):
618 Plugin.options(self, parser, env)
601 Plugin.options(self, parser, env)
619 parser.add_option('--doctest-tests', action='store_true',
602 parser.add_option('--doctest-tests', action='store_true',
@@ -716,35 +699,6 b' class ExtensionDoctest(doctests.Doctest):'
716 else:
699 else:
717 yield False # no tests to load
700 yield False # no tests to load
718
701
719 def wantFile(self,filename):
720 """Return whether the given filename should be scanned for tests.
721
722 Modified version that accepts extension modules as valid containers for
723 doctests.
724 """
725 #print '*** ipdoctest- wantFile:',filename # dbg
726
727 for pat in self.exclude_patterns:
728 if pat.search(filename):
729 # print '###>>> SKIP:',filename # dbg
730 return False
731
732 if is_extension_module(filename):
733 return True
734 else:
735 return doctests.Doctest.wantFile(self,filename)
736
737 def wantDirectory(self, directory):
738 """Return whether the given directory should be scanned for tests.
739
740 Modified version that supports exclusions.
741 """
742
743 for pat in self.exclude_patterns:
744 if pat.search(directory):
745 return False
746 return True
747
748
702
749 class IPythonDoctest(ExtensionDoctest):
703 class IPythonDoctest(ExtensionDoctest):
750 """Nose Plugin that supports doctests in extension modules.
704 """Nose Plugin that supports doctests in extension modules.
General Comments 0
You need to be logged in to leave comments. Login now