##// END OF EJS Templates
Split out iptestcontroller to control test process.
Thomas Kluyver -
Show More
@@ -0,0 +1,319 b''
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
3
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
6
7 """
8
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2009-2011 The IPython Development Team
11 #
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
15
16 #-----------------------------------------------------------------------------
17 # Imports
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
20
21 import multiprocessing.pool
22 import os
23 import signal
24 import sys
25 import subprocess
26 import tempfile
27 import time
28
29 from .iptest import have, special_test_suites
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
34 from IPython.utils.tempdir import TemporaryDirectory
35
36
37 class IPTester(object):
38 """Call that calls iptest or trial in a subprocess.
39 """
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
50 buffer_output = False
51
52 def __init__(self, runner='iptest', params=None):
53 """Create new test runner."""
54 if runner == 'iptest':
55 iptest_app = os.path.abspath(get_ipython_module_path('IPython.testing.iptest'))
56 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
57 else:
58 raise Exception('Not a valid test runner: %s' % repr(runner))
59 if params is None:
60 params = []
61 if isinstance(params, str):
62 params = [params]
63 self.params = params
64
65 # Assemble call
66 self.call_args = self.runner+self.params
67
68 # Find the section we're testing (IPython.foo)
69 for sect in self.params:
70 if sect.startswith('IPython') or sect in special_test_suites: break
71 else:
72 raise ValueError("Section not found", self.params)
73
74 if '--with-xunit' in self.call_args:
75
76 self.call_args.append('--xunit-file')
77 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
78 xunit_file = os.path.abspath(sect+'.xunit.xml')
79 if sys.platform == 'win32':
80 xunit_file = '"%s"' % xunit_file
81 self.call_args.append(xunit_file)
82
83 if '--with-xml-coverage' in self.call_args:
84 self.coverage_xml = os.path.abspath(sect+".coverage.xml")
85 self.call_args.remove('--with-xml-coverage')
86 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
87
88 # Store anything we start to clean up on deletion
89 self.processes = []
90
91 def _run_cmd(self):
92 with TemporaryDirectory() as IPYTHONDIR:
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
109 def run(self):
110 """Run the stored commands"""
111 try:
112 retcode = self._run_cmd()
113 except KeyboardInterrupt:
114 return -signal.SIGINT
115 except:
116 import traceback
117 traceback.print_exc()
118 return 1 # signal failure
119
120 if self.coverage_xml:
121 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
122 return retcode
123
124 def __del__(self):
125 """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 """
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
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
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
189
190 def report():
191 """Return a string with a summary report of test-related variables."""
192
193 out = [ sys_info(), '\n']
194
195 avail = []
196 not_avail = []
197
198 for k, is_avail in have.items():
199 if is_avail:
200 avail.append(k)
201 else:
202 not_avail.append(k)
203
204 if avail:
205 out.append('\nTools and libraries available at test time:\n')
206 avail.sort()
207 out.append(' ' + ' '.join(avail)+'\n')
208
209 if not_avail:
210 out.append('\nTools and libraries NOT available at test time:\n')
211 not_avail.sort()
212 out.append(' ' + ' '.join(not_avail)+'\n')
213
214 return ''.join(out)
215
216 def run_iptestall(inc_slow=False, fast=False):
217 """Run the entire IPython test suite by calling nose and trial.
218
219 This function constructs :class:`IPTester` instances for all IPython
220 modules and package and then runs each of them. This causes the modules
221 and packages of IPython to be tested each in their own subprocess using
222 nose.
223
224 Parameters
225 ----------
226
227 inc_slow : bool, optional
228 Include slow tests, like IPython.parallel. By default, these tests aren't
229 run.
230
231 fast : bool, option
232 Run the test suite in parallel, if True, using as many threads as there
233 are processors
234 """
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)
241
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)
249
250 # Run all test runners, tracking execution time
251 failed = []
252 t_start = time.time()
253
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)
269 t_end = time.time()
270 t_tests = t_end - t_start
271 nrunners = len(runners)
272 nfail = len(failed)
273 # summarize results
274 print()
275 print('*'*70)
276 print('Test suite completed for system with the following information:')
277 print(report())
278 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
279 print()
280 print('Status:')
281 if not failed:
282 print('OK')
283 else:
284 # If anything went wrong, point out what command to rerun manually to
285 # see the actual errors and individual summary
286 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
287 for name, failed_runner in failed:
288 print('-'*40)
289 print('Runner failed:',name)
290 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]
292 print(u' '.join(failed_call_args))
293 print()
294 # Ensure that our exit code indicates failure
295 sys.exit(1)
296
297
298 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)
316
317
318 if __name__ == '__main__':
319 main()
@@ -1,24 +1,24 b''
1 1 #!/usr/bin/env python
2 2 # -*- coding: utf-8 -*-
3 3 """IPython Test Suite Runner.
4 4 """
5 5
6 6 # The tests can't even run if nose isn't available, so might as well give the
7 7 # user a civilized error message in that case.
8 8
9 9 try:
10 10 import nose
11 11 except ImportError:
12 12 error = """\
13 13 ERROR: The IPython test suite requires nose to run.
14 14
15 15 Please install nose on your system first and try again.
16 16 For information on installing nose, see:
17 17 http://somethingaboutorange.com/mrl/projects/nose
18 18
19 19 Exiting."""
20 20 import sys
21 21 print >> sys.stderr, error
22 22 else:
23 from IPython.testing import iptest
24 iptest.main()
23 from IPython.testing import iptestcontroller
24 iptestcontroller.main()
@@ -1,656 +1,365 b''
1 1 # -*- coding: utf-8 -*-
2 2 """IPython Test Suite Runner.
3 3
4 4 This module provides a main entry point to a user script to test IPython
5 5 itself from the command line. There are two ways of running this script:
6 6
7 7 1. With the syntax `iptest all`. This runs our entire test suite by
8 8 calling this script (with different arguments) recursively. This
9 9 causes modules and package to be tested in different processes, using nose
10 10 or trial where appropriate.
11 11 2. With the regular nose syntax, like `iptest -vvs IPython`. In this form
12 12 the script simply calls nose, but with special command line flags and
13 13 plugins loaded.
14 14
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Copyright (C) 2009-2011 The IPython Development Team
19 19 #
20 20 # Distributed under the terms of the BSD License. The full license is in
21 21 # the file COPYING, distributed as part of this software.
22 22 #-----------------------------------------------------------------------------
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Imports
26 26 #-----------------------------------------------------------------------------
27 27 from __future__ import print_function
28 28
29 29 # Stdlib
30 30 import glob
31 31 import os
32 32 import os.path as path
33 import signal
34 33 import sys
35 import subprocess
36 import tempfile
37 import time
38 34 import warnings
39 import multiprocessing.pool
40 35
41 36 # Now, proceed to import nose itself
42 37 import nose.plugins.builtin
43 38 from nose.plugins.xunit import Xunit
44 39 from nose import SkipTest
45 40 from nose.core import TestProgram
46 41
47 42 # Our own imports
48 from IPython.utils import py3compat
49 43 from IPython.utils.importstring import import_item
50 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
51 from IPython.utils.process import pycmd2argv
52 from IPython.utils.sysinfo import sys_info
53 from IPython.utils.tempdir import TemporaryDirectory
44 from IPython.utils.path import get_ipython_package_dir
54 45 from IPython.utils.warn import warn
55 46
56 47 from IPython.testing import globalipapp
57 48 from IPython.testing.plugin.ipdoctest import IPythonDoctest
58 49 from IPython.external.decorators import KnownFailure, knownfailureif
59 50
60 51 pjoin = path.join
61 52
62 53
63 54 #-----------------------------------------------------------------------------
64 55 # Globals
65 56 #-----------------------------------------------------------------------------
66 57
67 58
68 59 #-----------------------------------------------------------------------------
69 60 # Warnings control
70 61 #-----------------------------------------------------------------------------
71 62
72 63 # Twisted generates annoying warnings with Python 2.6, as will do other code
73 64 # that imports 'sets' as of today
74 65 warnings.filterwarnings('ignore', 'the sets module is deprecated',
75 66 DeprecationWarning )
76 67
77 68 # This one also comes from Twisted
78 69 warnings.filterwarnings('ignore', 'the sha module is deprecated',
79 70 DeprecationWarning)
80 71
81 72 # Wx on Fedora11 spits these out
82 73 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
83 74 UserWarning)
84 75
85 76 # ------------------------------------------------------------------------------
86 77 # Monkeypatch Xunit to count known failures as skipped.
87 78 # ------------------------------------------------------------------------------
88 79 def monkeypatch_xunit():
89 80 try:
90 81 knownfailureif(True)(lambda: None)()
91 82 except Exception as e:
92 83 KnownFailureTest = type(e)
93 84
94 85 def addError(self, test, err, capt=None):
95 86 if issubclass(err[0], KnownFailureTest):
96 87 err = (SkipTest,) + err[1:]
97 88 return self.orig_addError(test, err, capt)
98 89
99 90 Xunit.orig_addError = Xunit.addError
100 91 Xunit.addError = addError
101 92
102 93 #-----------------------------------------------------------------------------
103 94 # Logic for skipping doctests
104 95 #-----------------------------------------------------------------------------
105 96 def extract_version(mod):
106 97 return mod.__version__
107 98
108 99 def test_for(item, min_version=None, callback=extract_version):
109 100 """Test to see if item is importable, and optionally check against a minimum
110 101 version.
111 102
112 103 If min_version is given, the default behavior is to check against the
113 104 `__version__` attribute of the item, but specifying `callback` allows you to
114 105 extract the value you are interested in. e.g::
115 106
116 107 In [1]: import sys
117 108
118 109 In [2]: from IPython.testing.iptest import test_for
119 110
120 111 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
121 112 Out[3]: True
122 113
123 114 """
124 115 try:
125 116 check = import_item(item)
126 117 except (ImportError, RuntimeError):
127 118 # GTK reports Runtime error if it can't be initialized even if it's
128 119 # importable.
129 120 return False
130 121 else:
131 122 if min_version:
132 123 if callback:
133 124 # extra processing step to get version to compare
134 125 check = callback(check)
135 126
136 127 return check >= min_version
137 128 else:
138 129 return True
139 130
140 131 # Global dict where we can store information on what we have and what we don't
141 132 # have available at test run time
142 133 have = {}
143 134
144 135 have['curses'] = test_for('_curses')
145 136 have['matplotlib'] = test_for('matplotlib')
146 137 have['numpy'] = test_for('numpy')
147 138 have['pexpect'] = test_for('IPython.external.pexpect')
148 139 have['pymongo'] = test_for('pymongo')
149 140 have['pygments'] = test_for('pygments')
150 141 have['qt'] = test_for('IPython.external.qt')
151 142 have['rpy2'] = test_for('rpy2')
152 143 have['sqlite3'] = test_for('sqlite3')
153 144 have['cython'] = test_for('Cython')
154 145 have['oct2py'] = test_for('oct2py')
155 146 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
156 147 have['jinja2'] = test_for('jinja2')
157 148 have['wx'] = test_for('wx')
158 149 have['wx.aui'] = test_for('wx.aui')
159 150 have['azure'] = test_for('azure')
160 151 have['sphinx'] = test_for('sphinx')
161 152
162 153 min_zmq = (2,1,11)
163 154
164 155 have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x())
165 156
166 157 #-----------------------------------------------------------------------------
167 158 # Functions and classes
168 159 #-----------------------------------------------------------------------------
169 160
170 def report():
171 """Return a string with a summary report of test-related variables."""
172
173 out = [ sys_info(), '\n']
174
175 avail = []
176 not_avail = []
177
178 for k, is_avail in have.items():
179 if is_avail:
180 avail.append(k)
181 else:
182 not_avail.append(k)
183
184 if avail:
185 out.append('\nTools and libraries available at test time:\n')
186 avail.sort()
187 out.append(' ' + ' '.join(avail)+'\n')
188
189 if not_avail:
190 out.append('\nTools and libraries NOT available at test time:\n')
191 not_avail.sort()
192 out.append(' ' + ' '.join(not_avail)+'\n')
193
194 return ''.join(out)
195
196 161
197 162 def make_exclude():
198 163 """Make patterns of modules and packages to exclude from testing.
199 164
200 165 For the IPythonDoctest plugin, we need to exclude certain patterns that
201 166 cause testing problems. We should strive to minimize the number of
202 167 skipped modules, since this means untested code.
203 168
204 169 These modules and packages will NOT get scanned by nose at all for tests.
205 170 """
206 171 # Simple utility to make IPython paths more readably, we need a lot of
207 172 # these below
208 173 ipjoin = lambda *paths: pjoin('IPython', *paths)
209 174
210 175 exclusions = [ipjoin('external'),
211 176 ipjoin('quarantine'),
212 177 ipjoin('deathrow'),
213 178 # This guy is probably attic material
214 179 ipjoin('testing', 'mkdoctests'),
215 180 # Testing inputhook will need a lot of thought, to figure out
216 181 # how to have tests that don't lock up with the gui event
217 182 # loops in the picture
218 183 ipjoin('lib', 'inputhook'),
219 184 # Config files aren't really importable stand-alone
220 185 ipjoin('config', 'profile'),
221 186 # The notebook 'static' directory contains JS, css and other
222 187 # files for web serving. Occasionally projects may put a .py
223 188 # file in there (MathJax ships a conf.py), so we might as
224 189 # well play it safe and skip the whole thing.
225 190 ipjoin('html', 'static'),
226 191 ipjoin('html', 'fabfile'),
227 192 ]
228 193 if not have['sqlite3']:
229 194 exclusions.append(ipjoin('core', 'tests', 'test_history'))
230 195 exclusions.append(ipjoin('core', 'history'))
231 196 if not have['wx']:
232 197 exclusions.append(ipjoin('lib', 'inputhookwx'))
233 198
234 199 if 'IPython.kernel.inprocess' not in sys.argv:
235 200 exclusions.append(ipjoin('kernel', 'inprocess'))
236 201
237 202 # FIXME: temporarily disable autoreload tests, as they can produce
238 203 # spurious failures in subsequent tests (cythonmagic).
239 204 exclusions.append(ipjoin('extensions', 'autoreload'))
240 205 exclusions.append(ipjoin('extensions', 'tests', 'test_autoreload'))
241 206
242 207 # We do this unconditionally, so that the test suite doesn't import
243 208 # gtk, changing the default encoding and masking some unicode bugs.
244 209 exclusions.append(ipjoin('lib', 'inputhookgtk'))
245 210 exclusions.append(ipjoin('kernel', 'zmq', 'gui', 'gtkembed'))
246 211
247 212 #Also done unconditionally, exclude nbconvert directories containing
248 213 #config files used to test. Executing the config files with iptest would
249 214 #cause an exception.
250 215 exclusions.append(ipjoin('nbconvert', 'tests', 'files'))
251 216 exclusions.append(ipjoin('nbconvert', 'exporters', 'tests', 'files'))
252 217
253 218 # These have to be skipped on win32 because the use echo, rm, cd, etc.
254 219 # See ticket https://github.com/ipython/ipython/issues/87
255 220 if sys.platform == 'win32':
256 221 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
257 222 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
258 223
259 224 if not have['pexpect']:
260 225 exclusions.extend([ipjoin('lib', 'irunner'),
261 226 ipjoin('lib', 'tests', 'test_irunner'),
262 227 ipjoin('terminal', 'console'),
263 228 ])
264 229
265 230 if not have['zmq']:
266 231 exclusions.append(ipjoin('lib', 'kernel'))
267 232 exclusions.append(ipjoin('kernel'))
268 233 exclusions.append(ipjoin('qt'))
269 234 exclusions.append(ipjoin('html'))
270 235 exclusions.append(ipjoin('consoleapp.py'))
271 236 exclusions.append(ipjoin('terminal', 'console'))
272 237 exclusions.append(ipjoin('parallel'))
273 238 elif not have['qt'] or not have['pygments']:
274 239 exclusions.append(ipjoin('qt'))
275 240
276 241 if not have['pymongo']:
277 242 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
278 243 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
279 244
280 245 if not have['matplotlib']:
281 246 exclusions.extend([ipjoin('core', 'pylabtools'),
282 247 ipjoin('core', 'tests', 'test_pylabtools'),
283 248 ipjoin('kernel', 'zmq', 'pylab'),
284 249 ])
285 250
286 251 if not have['cython']:
287 252 exclusions.extend([ipjoin('extensions', 'cythonmagic')])
288 253 exclusions.extend([ipjoin('extensions', 'tests', 'test_cythonmagic')])
289 254
290 255 if not have['oct2py']:
291 256 exclusions.extend([ipjoin('extensions', 'octavemagic')])
292 257 exclusions.extend([ipjoin('extensions', 'tests', 'test_octavemagic')])
293 258
294 259 if not have['tornado']:
295 260 exclusions.append(ipjoin('html'))
296 261 exclusions.append(ipjoin('nbconvert', 'post_processors', 'serve'))
297 262 exclusions.append(ipjoin('nbconvert', 'post_processors', 'tests', 'test_serve'))
298 263
299 264 if not have['jinja2']:
300 265 exclusions.append(ipjoin('html', 'notebookapp'))
301 266
302 267 if not have['rpy2'] or not have['numpy']:
303 268 exclusions.append(ipjoin('extensions', 'rmagic'))
304 269 exclusions.append(ipjoin('extensions', 'tests', 'test_rmagic'))
305 270
306 271 if not have['azure']:
307 272 exclusions.append(ipjoin('html', 'services', 'notebooks', 'azurenbmanager'))
308 273
309 274 if not all((have['pygments'], have['jinja2'], have['sphinx'])):
310 275 exclusions.append(ipjoin('nbconvert'))
311 276
312 277 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
313 278 if sys.platform == 'win32':
314 279 exclusions = [s.replace('\\','\\\\') for s in exclusions]
315 280
316 281 # check for any exclusions that don't seem to exist:
317 282 parent, _ = os.path.split(get_ipython_package_dir())
318 283 for exclusion in exclusions:
319 284 if exclusion.endswith(('deathrow', 'quarantine')):
320 285 # ignore deathrow/quarantine, which exist in dev, but not install
321 286 continue
322 287 fullpath = pjoin(parent, exclusion)
323 288 if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
324 289 warn("Excluding nonexistent file: %r" % exclusion)
325 290
326 291 return exclusions
327 292
328
329 class IPTester(object):
330 """Call that calls iptest or trial in a subprocess.
331 """
332 #: string, name of test runner that will be called
333 runner = None
334 #: list, parameters for test runner
335 params = None
336 #: list, arguments of system call to be made to call test runner
337 call_args = None
338 #: list, subprocesses we start (for cleanup)
339 processes = None
340 #: str, coverage xml output file
341 coverage_xml = None
342 buffer_output = False
343
344 def __init__(self, runner='iptest', params=None):
345 """Create new test runner."""
346 p = os.path
347 if runner == 'iptest':
348 iptest_app = os.path.abspath(get_ipython_module_path('IPython.testing.iptest'))
349 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
350 else:
351 raise Exception('Not a valid test runner: %s' % repr(runner))
352 if params is None:
353 params = []
354 if isinstance(params, str):
355 params = [params]
356 self.params = params
357
358 # Assemble call
359 self.call_args = self.runner+self.params
360
361 # Find the section we're testing (IPython.foo)
362 for sect in self.params:
363 if sect.startswith('IPython') or sect in special_test_suites: break
364 else:
365 raise ValueError("Section not found", self.params)
366
367 if '--with-xunit' in self.call_args:
368
369 self.call_args.append('--xunit-file')
370 # FIXME: when Windows uses subprocess.call, these extra quotes are unnecessary:
371 xunit_file = path.abspath(sect+'.xunit.xml')
372 if sys.platform == 'win32':
373 xunit_file = '"%s"' % xunit_file
374 self.call_args.append(xunit_file)
375
376 if '--with-xml-coverage' in self.call_args:
377 self.coverage_xml = path.abspath(sect+".coverage.xml")
378 self.call_args.remove('--with-xml-coverage')
379 self.call_args = ["coverage", "run", "--source="+sect] + self.call_args[1:]
380
381 # Store anything we start to clean up on deletion
382 self.processes = []
383
384 def _run_cmd(self):
385 with TemporaryDirectory() as IPYTHONDIR:
386 env = os.environ.copy()
387 env['IPYTHONDIR'] = IPYTHONDIR
388 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
389 output = subprocess.PIPE if self.buffer_output else None
390 subp = subprocess.Popen(self.call_args, stdout=output,
391 stderr=output, env=env)
392 self.processes.append(subp)
393 # If this fails, the process will be left in self.processes and
394 # cleaned up later, but if the wait call succeeds, then we can
395 # clear the stored process.
396 retcode = subp.wait()
397 self.processes.pop()
398 self.stdout = subp.stdout
399 self.stderr = subp.stderr
400 return retcode
401
402 def run(self):
403 """Run the stored commands"""
404 try:
405 retcode = self._run_cmd()
406 except KeyboardInterrupt:
407 return -signal.SIGINT
408 except:
409 import traceback
410 traceback.print_exc()
411 return 1 # signal failure
412
413 if self.coverage_xml:
414 subprocess.call(["coverage", "xml", "-o", self.coverage_xml])
415 return retcode
416
417 def __del__(self):
418 """Cleanup on exit by killing any leftover processes."""
419 for subp in self.processes:
420 if subp.poll() is not None:
421 continue # process is already dead
422
423 try:
424 print('Cleaning up stale PID: %d' % subp.pid)
425 subp.kill()
426 except: # (OSError, WindowsError) ?
427 # This is just a best effort, if we fail or the process was
428 # really gone, ignore it.
429 pass
430 else:
431 for i in range(10):
432 if subp.poll() is None:
433 time.sleep(0.1)
434 else:
435 break
436
437 if subp.poll() is None:
438 # The process did not die...
439 print('... failed. Manual cleanup may be required.')
440
441
442 293 special_test_suites = {
443 294 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
444 295 }
445
446 def make_runners(inc_slow=False):
447 """Define the top-level packages that need to be tested.
448 """
449
450 # Packages to be tested via nose, that only depend on the stdlib
451 nose_pkg_names = ['config', 'core', 'extensions', 'lib', 'terminal',
452 'testing', 'utils', 'nbformat']
453
454 if have['qt']:
455 nose_pkg_names.append('qt')
456
457 if have['tornado']:
458 nose_pkg_names.append('html')
459
460 if have['zmq']:
461 nose_pkg_names.insert(0, 'kernel')
462 nose_pkg_names.insert(1, 'kernel.inprocess')
463 if inc_slow:
464 nose_pkg_names.insert(0, 'parallel')
465
466 if all((have['pygments'], have['jinja2'], have['sphinx'])):
467 nose_pkg_names.append('nbconvert')
468
469 # For debugging this code, only load quick stuff
470 #nose_pkg_names = ['core', 'extensions'] # dbg
471
472 # Make fully qualified package names prepending 'IPython.' to our name lists
473 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
474
475 # Make runners
476 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
477
478 for name in special_test_suites:
479 runners.append((name, IPTester('iptest', params=name)))
480
481 return runners
482 296
483 297
484 298 def run_iptest():
485 299 """Run the IPython test suite using nose.
486 300
487 301 This function is called when this script is **not** called with the form
488 302 `iptest all`. It simply calls nose with appropriate command line flags
489 303 and accepts all of the standard nose arguments.
490 304 """
491 305 # Apply our monkeypatch to Xunit
492 306 if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
493 307 monkeypatch_xunit()
494 308
495 309 warnings.filterwarnings('ignore',
496 310 'This will be removed soon. Use IPython.testing.util instead')
497 311
498 312 if sys.argv[1] in special_test_suites:
499 313 sys.argv[1:2] = special_test_suites[sys.argv[1]]
500 314 special_suite = True
501 315 else:
502 316 special_suite = False
503 317
504 318 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
505 319
506 320 '--with-ipdoctest',
507 321 '--ipdoctest-tests','--ipdoctest-extension=txt',
508 322
509 323 # We add --exe because of setuptools' imbecility (it
510 324 # blindly does chmod +x on ALL files). Nose does the
511 325 # right thing and it tries to avoid executables,
512 326 # setuptools unfortunately forces our hand here. This
513 327 # has been discussed on the distutils list and the
514 328 # setuptools devs refuse to fix this problem!
515 329 '--exe',
516 330 ]
517 331 if '-a' not in argv and '-A' not in argv:
518 332 argv = argv + ['-a', '!crash']
519 333
520 334 if nose.__version__ >= '0.11':
521 335 # I don't fully understand why we need this one, but depending on what
522 336 # directory the test suite is run from, if we don't give it, 0 tests
523 337 # get run. Specifically, if the test suite is run from the source dir
524 338 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
525 339 # even if the same call done in this directory works fine). It appears
526 340 # that if the requested package is in the current dir, nose bails early
527 341 # by default. Since it's otherwise harmless, leave it in by default
528 342 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
529 343 argv.append('--traverse-namespace')
530 344
531 345 # use our plugin for doctesting. It will remove the standard doctest plugin
532 346 # if it finds it enabled
533 347 ipdt = IPythonDoctest() if special_suite else IPythonDoctest(make_exclude())
534 348 plugins = [ipdt, KnownFailure()]
535 349
536 350 # We need a global ipython running in this process, but the special
537 351 # in-process group spawns its own IPython kernels, so for *that* group we
538 352 # must avoid also opening the global one (otherwise there's a conflict of
539 353 # singletons). Ultimately the solution to this problem is to refactor our
540 354 # assumptions about what needs to be a singleton and what doesn't (app
541 355 # objects should, individual shells shouldn't). But for now, this
542 356 # workaround allows the test suite for the inprocess module to complete.
543 357 if not 'IPython.kernel.inprocess' in sys.argv:
544 358 globalipapp.start_ipython()
545 359
546 360 # Now nose can run
547 361 TestProgram(argv=argv, addplugins=plugins)
548 362
549 def do_run(x):
550 print('IPython test group:',x[0])
551 ret = x[1].run()
552 return ret
553
554 def run_iptestall(inc_slow=False, fast=False):
555 """Run the entire IPython test suite by calling nose and trial.
556
557 This function constructs :class:`IPTester` instances for all IPython
558 modules and package and then runs each of them. This causes the modules
559 and packages of IPython to be tested each in their own subprocess using
560 nose.
561
562 Parameters
563 ----------
564
565 inc_slow : bool, optional
566 Include slow tests, like IPython.parallel. By default, these tests aren't
567 run.
568
569 fast : bool, option
570 Run the test suite in parallel, if True, using as many threads as there
571 are processors
572 """
573 if fast:
574 p = multiprocessing.pool.ThreadPool()
575 else:
576 p = multiprocessing.pool.ThreadPool(1)
577
578 runners = make_runners(inc_slow=inc_slow)
579
580 # Run the test runners in a temporary dir so we can nuke it when finished
581 # to clean up any junk files left over by accident. This also makes it
582 # robust against being run in non-writeable directories by mistake, as the
583 # temp dir will always be user-writeable.
584 curdir = os.getcwdu()
585 testdir = tempfile.gettempdir()
586 os.chdir(testdir)
587
588 # Run all test runners, tracking execution time
589 failed = []
590 t_start = time.time()
591
592 try:
593 all_res = p.map(do_run, runners)
594 print('*'*70)
595 for ((name, runner), res) in zip(runners, all_res):
596 tgroup = 'IPython test group: ' + name
597 res_string = 'OK' if res == 0 else 'FAILED'
598 res_string = res_string.rjust(70 - len(tgroup), '.')
599 print(tgroup + res_string)
600 if res:
601 failed.append( (name, runner) )
602 if res == -signal.SIGINT:
603 print("Interrupted")
604 break
605 finally:
606 os.chdir(curdir)
607 t_end = time.time()
608 t_tests = t_end - t_start
609 nrunners = len(runners)
610 nfail = len(failed)
611 # summarize results
612 print()
613 print('*'*70)
614 print('Test suite completed for system with the following information:')
615 print(report())
616 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
617 print()
618 print('Status:')
619 if not failed:
620 print('OK')
621 else:
622 # If anything went wrong, point out what command to rerun manually to
623 # see the actual errors and individual summary
624 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
625 for name, failed_runner in failed:
626 print('-'*40)
627 print('Runner failed:',name)
628 print('You may wish to rerun this one individually, with:')
629 failed_call_args = [py3compat.cast_unicode(x) for x in failed_runner.call_args]
630 print(u' '.join(failed_call_args))
631 print()
632 # Ensure that our exit code indicates failure
633 sys.exit(1)
634
635
636 def main():
637 for arg in sys.argv[1:]:
638 if arg.startswith('IPython') or arg in special_test_suites:
639 # This is in-process
640 run_iptest()
641 else:
642 inc_slow = "--all" in sys.argv
643 if inc_slow:
644 sys.argv.remove("--all")
645
646 fast = "--fast" in sys.argv
647 if fast:
648 sys.argv.remove("--fast")
649 IPTester.buffer_output = True
650
651 # This starts subprocesses
652 run_iptestall(inc_slow=inc_slow, fast=fast)
653
654 363
655 364 if __name__ == '__main__':
656 main()
365 run_iptest()
General Comments 0
You need to be logged in to leave comments. Login now