##// 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 ]
161
167
162 def make_exclude():
168 class TestSection(object):
163 """Make patterns of modules and packages to exclude from testing.
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
176 def exclude(self, module):
166 cause testing problems. We should strive to minimize the number of
177 if not module.startswith('IPython'):
167 skipped modules, since this means untested code.
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.
181 def requires(self, *packages):
170 """
182 self.dependencies.extend(packages)
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
183
175 exclusions = [ipjoin('external'),
184 @property
176 ipjoin('quarantine'),
185 def will_run(self):
177 ipjoin('deathrow'),
186 return self.enabled and all(have[p] for p in self.dependencies)
178 # This guy is probably attic material
187
179 ipjoin('testing', 'mkdoctests'),
188 # Name -> (include, exclude, dependencies_met)
180 # Testing inputhook will need a lot of thought, to figure out
189 test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
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
190
199 if 'IPython.kernel.inprocess' not in sys.argv:
191 # Exclusions and dependencies
200 exclusions.append(ipjoin('kernel', 'inprocess'))
192 # ---------------------------
201
193
202 # FIXME: temporarily disable autoreload tests, as they can produce
194 # core:
203 # spurious failures in subsequent tests (cythonmagic).
195 sec = test_sections['core']
204 exclusions.append(ipjoin('extensions', 'autoreload'))
196 if not have['sqlite3']:
205 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
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 # We do this unconditionally, so that the test suite doesn't import
212 # We do this unconditionally, so that the test suite doesn't import
208 # gtk, changing the default encoding and masking some unicode bugs.
213 # gtk, changing the default encoding and masking some unicode bugs.
209 exclusions.append(ipjoin('lib', 'inputhookgtk'))
214 sec.exclude('inputhookgtk')
210 exclusions.append(ipjoin('kernel', 'zmq', 'gui', 'gtkembed'))
215 # Testing inputhook will need a lot of thought, to figure out
211
216 # how to have tests that don't lock up with the gui event
212 #Also done unconditionally, exclude nbconvert directories containing
217 # loops in the picture
213 #config files used to test. Executing the config files with iptest would
218 sec.exclude('inputhook')
214 #cause an exception.
215 exclusions.append(ipjoin('nbconvert', 'tests', 'files'))
216 exclusions.append(ipjoin('nbconvert', 'exporters', 'tests', 'files'))
217
219
220 # testing:
221 sec = test_sections['lib']
222 # This guy is probably attic material
223 sec.exclude('mkdoctests')
218 # These have to be skipped on win32 because the use echo, rm, cd, etc.
224 # These have to be skipped on win32 because the use echo, rm, cd, etc.
219 # See ticket https://github.com/ipython/ipython/issues/87
225 # See ticket https://github.com/ipython/ipython/issues/87
220 if sys.platform == 'win32':
226 if sys.platform == 'win32':
221 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
227 sec.exclude('plugin.test_exampleip')
222 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
228 sec.exclude('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
229
230 if not have['zmq']:
230 # terminal:
231 exclusions.append(ipjoin('lib', 'kernel'))
231 if (not have['pexpect']) or (not have['zmq']):
232 exclusions.append(ipjoin('kernel'))
232 test_sections['terminal'].exclude('console')
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
233
234 # parallel
235 sec = test_sections['parallel']
236 sec.requires('zmq')
241 if not have['pymongo']:
237 if not have['pymongo']:
242 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
238 sec.exclude('controller.mongodb')
243 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
239 sec.exclude('tests.test_mongodb')
244
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 if not have['matplotlib']:
248 if not have['matplotlib']:
246 exclusions.extend([ipjoin('core', 'pylabtools'),
249 sec.exclude('zmq.pylab')
247 ipjoin('core', 'tests', 'test_pylabtools'),
248 ipjoin('kernel', 'zmq', 'pylab'),
249 ])
250
250
251 if not have['cython']:
251 # kernel.inprocess:
252 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
252 test_sections['kernel.inprocess'].requires('zmq')
253 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
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 if not have['oct2py']:
259 if not have['oct2py']:
256 exclusions.extend([ipjoin('extensions', 'octavemagic')])
260 sec.exclude('octavemagic')
257 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
261 sec.exclude('tests.test_octavemagic')
258
262 if not have['rpy2'] or not have['numpy']:
259 if not have['tornado']:
263 sec.exclude('rmagic')
260 exclusions.append(ipjoin('html'))
264 sec.exclude('tests.test_rmagic')
261 exclusions.append(ipjoin('nbconvert', 'post_processors', 'serve'))
265 # autoreload does some strange stuff, so move it to its own test section
262 exclusions.append(ipjoin('nbconvert', 'post_processors', 'tests', 'test_serve'))
266 sec.exclude('autoreload')
263
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 if not have['jinja2']:
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']:
289 # config:
268 exclusions.append(ipjoin('extensions', 'rmagic'))
290 # Config files aren't really importable stand-alone
269 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
291 test_sections['config'].exclude('profile')
270
292
271 if not have['azure']:
293 # nbconvert:
272 exclusions.append(ipjoin('html', 'services', 'notebooks', 'azurenbmanager'))
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'])):
301 #-----------------------------------------------------------------------------
275 exclusions.append(ipjoin('nbconvert'))
302 # Functions and classes
303 #-----------------------------------------------------------------------------
276
304
277 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
305 def check_exclusions_exist():
278 if sys.platform == 'win32':
306 parent = os.path.dirname(get_ipython_package_dir())
279 exclusions = [s.replace('\\','\\\\') for s in exclusions]
307 for sec in test_sections:
280
308 for pattern in sec.exclusions:
281 # check for any exclusions that don't seem to exist:
309 fullpath = pjoin(parent, pattern)
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 + '.*'):
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 = {
314 class ExclusionPlugin(Plugin):
294 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
315 """A nose plugin to effect our exclusions of files and directories.
295 }
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 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:
72
75
73
76 self.call_args.append('--xunit-file')
74 def launch(self):
77 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
75 # print('*** ENV:', self.env) # dbg
78 xunit_file = os.path.abspath(sect+'.xunit.xml')
76 # print('*** CMD:', self.cmd) # dbg
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:
93 env = os.environ.copy()
77 env = os.environ.copy()
94 env['IPYTHONDIR'] = IPYTHONDIR
78 env.update(self.env)
95 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
96 output = subprocess.PIPE if self.buffer_output else None
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 stderr=output, env=env)
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 def run(self):
83 def run(self):
110 """Run the stored commands"""
84 """Run the stored commands"""
@@ -121,11 +95,11 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
103
130 try:
104 try:
131 print('Cleaning up stale PID: %d' % subp.pid)
105 print('Cleaning up stale PID: %d' % subp.pid)
@@ -145,47 +119,37 b' class IPTester(object):'
145 # The process did not die...
119 # The process did not die...
146 print('... failed. Manual cleanup may be required.')
120 print('... failed. Manual cleanup may be required.')
147
121
148 def make_runners(inc_slow=False):
122 for td in self.dirs:
149 """Define the top-level packages that need to be tested.
123 td.cleanup()
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')
167
124
168 if all((have['pygments'], have['jinja2'], have['sphinx'])):
125 __del__ = cleanup
169 nose_pkg_names.append('nbconvert')
170
126
171 # For debugging this code, only load quick stuff
127 def test_controllers_to_run(inc_slow=False):
172 #nose_pkg_names = ['core', 'extensions'] # dbg
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
137 def do_run(controller):
175 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
138 try:
176
139 try:
177 # Make runners
140 controller.launch()
178 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
141 except Exception:
179
142 import traceback
180 for name in special_test_suites:
143 traceback.print_exc()
181 runners.append((name, IPTester('iptest', params=name)))
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):
150 except KeyboardInterrupt:
186 print('IPython test group:',x[0])
151 controller.cleanup()
187 ret = x[1].run()
152 return controller, -signal.SIGINT
188 return ret
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:
255 all_res = p.map(do_run, runners)
256 print('*'*70)
209 print('*'*70)
257 for ((name, runner), res) in zip(runners, all_res):
210 for (controller, res) in pool.imap_unordered(do_run, controllers):
258 tgroup = 'IPython test group: ' + name
211 tgroup = 'IPython test group: ' + controller.section
259 res_string = 'OK' if res == 0 else 'FAILED'
212 res_string = 'OK' if res == 0 else 'FAILED'
260 res_string = res_string.rjust(70 - len(tgroup), '.')
213 res_string = res_string.rjust(70 - len(tgroup), '.')
261 print(tgroup + res_string)
214 print(tgroup + res_string)
262 if res:
215 if res:
263 failed.append( (name, runner) )
216 failed.append(controller)
264 if res == -signal.SIGINT:
217 if res == -signal.SIGINT:
265 print("Interrupted")
218 print("Interrupted")
266 break
219 break
267 finally:
220
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:
301 from .iptest import run_iptest
252 from .iptest import run_iptest
302 # This is in-process
253 # This is in-process
303 run_iptest()
254 run_iptest()
304 else:
255 return
305 inc_slow = "--all" in sys.argv
256
306 if inc_slow:
257 parser = argparse.ArgumentParser(description='Run IPython test suite')
307 sys.argv.remove("--all")
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
267 options = parser.parse_args()
310 if fast:
311 sys.argv.remove("--fast")
312 IPTester.buffer_output = True
313
268
314 # This starts subprocesses
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 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