##// END OF EJS Templates
Improve test suite robustness by cleaning up stale processes when possible.
Fernando Perez -
Show More
@@ -1,347 +1,384 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) or trial 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 For now, this script requires that both nose and twisted are installed. This
16 16 will change in the future.
17 17 """
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Module imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 import os
24 24 import os.path as path
25 import signal
25 26 import sys
26 27 import subprocess
27 28 import tempfile
28 29 import time
29 30 import warnings
30 31
31 32 import nose.plugins.builtin
32 33 from nose.core import TestProgram
33 34
34 35 from IPython.utils import genutils
35 36 from IPython.utils.platutils import find_cmd, FindCmdError
36 37
37 38 pjoin = path.join
38 39
39 40 #-----------------------------------------------------------------------------
40 41 # Warnings control
41 42 #-----------------------------------------------------------------------------
42 43 # Twisted generates annoying warnings with Python 2.6, as will do other code
43 44 # that imports 'sets' as of today
44 45 warnings.filterwarnings('ignore', 'the sets module is deprecated',
45 46 DeprecationWarning )
46 47
47 48 #-----------------------------------------------------------------------------
48 49 # Logic for skipping doctests
49 50 #-----------------------------------------------------------------------------
50 51
51 52 def test_for(mod):
52 53 """Test to see if mod is importable."""
53 54 try:
54 55 __import__(mod)
55 56 except ImportError:
56 57 return False
57 58 else:
58 59 return True
59 60
60 61 have_curses = test_for('_curses')
61 62 have_wx = test_for('wx')
62 63 have_wx_aui = test_for('wx.aui')
63 64 have_zi = test_for('zope.interface')
64 65 have_twisted = test_for('twisted')
65 66 have_foolscap = test_for('foolscap')
66 67 have_objc = test_for('objc')
67 68 have_pexpect = test_for('pexpect')
68 69 have_gtk = test_for('gtk')
69 70 have_gobject = test_for('gobject')
70 71
71 72
72 73 def make_exclude():
73 74
74 75 # For the IPythonDoctest plugin, we need to exclude certain patterns that
75 76 # cause testing problems. We should strive to minimize the number of
76 77 # skipped modules, since this means untested code. As the testing
77 78 # machinery solidifies, this list should eventually become empty.
78 79 EXCLUDE = [pjoin('IPython', 'external'),
79 80 pjoin('IPython', 'frontend', 'process', 'winprocess.py'),
80 81 pjoin('IPython_doctest_plugin'),
81 82 pjoin('IPython', 'quarantine'),
82 83 pjoin('IPython', 'deathrow'),
83 84 pjoin('IPython', 'testing', 'attic'),
84 85 pjoin('IPython', 'testing', 'tools'),
85 86 pjoin('IPython', 'testing', 'mkdoctests'),
86 87 pjoin('IPython', 'lib', 'inputhook')
87 88 ]
88 89
89 90 if not have_wx:
90 91 EXCLUDE.append(pjoin('IPython', 'gui'))
91 92 EXCLUDE.append(pjoin('IPython', 'frontend', 'wx'))
92 93 EXCLUDE.append(pjoin('IPython', 'lib', 'inputhookwx'))
93 94
94 95 if not have_gtk or not have_gobject:
95 96 EXCLUDE.append(pjoin('IPython', 'lib', 'inputhookgtk'))
96 97
97 98 if not have_wx_aui:
98 99 EXCLUDE.append(pjoin('IPython', 'gui', 'wx', 'wxIPython'))
99 100
100 101 if not have_objc:
101 102 EXCLUDE.append(pjoin('IPython', 'frontend', 'cocoa'))
102 103
103 104 if not sys.platform == 'win32':
104 105 EXCLUDE.append(pjoin('IPython', 'utils', 'platutils_win32'))
105 106
106 107 # These have to be skipped on win32 because the use echo, rm, cd, etc.
107 108 # See ticket https://bugs.launchpad.net/bugs/366982
108 109 if sys.platform == 'win32':
109 110 EXCLUDE.append(pjoin('IPython', 'testing', 'plugin', 'test_exampleip'))
110 111 EXCLUDE.append(pjoin('IPython', 'testing', 'plugin', 'dtexample'))
111 112
112 113 if not os.name == 'posix':
113 114 EXCLUDE.append(pjoin('IPython', 'utils', 'platutils_posix'))
114 115
115 116 if not have_pexpect:
116 117 EXCLUDE.append(pjoin('IPython', 'scripts', 'irunner'))
117 118
118 119 # This is scary. We still have things in frontend and testing that
119 120 # are being tested by nose that use twisted. We need to rethink
120 121 # how we are isolating dependencies in testing.
121 122 if not (have_twisted and have_zi and have_foolscap):
122 123 EXCLUDE.append(pjoin('IPython', 'frontend', 'asyncfrontendbase'))
123 124 EXCLUDE.append(pjoin('IPython', 'frontend', 'prefilterfrontend'))
124 125 EXCLUDE.append(pjoin('IPython', 'frontend', 'frontendbase'))
125 126 EXCLUDE.append(pjoin('IPython', 'frontend', 'linefrontendbase'))
126 127 EXCLUDE.append(pjoin('IPython', 'frontend', 'tests',
127 128 'test_linefrontend'))
128 129 EXCLUDE.append(pjoin('IPython', 'frontend', 'tests',
129 130 'test_frontendbase'))
130 131 EXCLUDE.append(pjoin('IPython', 'frontend', 'tests',
131 132 'test_prefilterfrontend'))
132 133 EXCLUDE.append(pjoin('IPython', 'frontend', 'tests',
133 134 'test_asyncfrontendbase')),
134 135 EXCLUDE.append(pjoin('IPython', 'testing', 'parametric'))
135 136 EXCLUDE.append(pjoin('IPython', 'testing', 'util'))
136 137
137 138 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
138 139 if sys.platform == 'win32':
139 140 EXCLUDE = [s.replace('\\','\\\\') for s in EXCLUDE]
140 141
141 142 return EXCLUDE
142 143
143 144
144 145 #-----------------------------------------------------------------------------
145 146 # Functions and classes
146 147 #-----------------------------------------------------------------------------
147 148
148 149 class IPTester(object):
149 150 """Call that calls iptest or trial in a subprocess.
150 151 """
152 #: string, name of test runner that will be called
153 runner = None
154 #: list, parameters for test runner
155 params = None
156 #: list, arguments of system call to be made to call test runner
157 call_args = None
158 #: list, process ids of subprocesses we start (for cleanup)
159 pids = None
160
151 161 def __init__(self,runner='iptest',params=None):
152 """ """
162 """Create new test runner."""
153 163 if runner == 'iptest':
154 164 # Find our own 'iptest' script OS-level entry point
155 165 try:
156 166 iptest_path = find_cmd('iptest')
157 167 except FindCmdError:
158 168 # Script not installed (may be the case for testing situations
159 169 # that are running from a source tree only), pull from internal
160 170 # path:
161 171 iptest_path = pjoin(genutils.get_ipython_package_dir(),
162 172 'scripts','iptest')
163 173 self.runner = [iptest_path,'-v']
164 174 else:
165 175 self.runner = [find_cmd('trial')]
166 176 if params is None:
167 177 params = []
168 178 if isinstance(params,str):
169 179 params = [params]
170 180 self.params = params
171 181
172 182 # Assemble call
173 183 self.call_args = self.runner+self.params
174 184
185 # Store pids of anything we start to clean up on deletion, if possible
186 # (on posix only, since win32 has no os.kill)
187 self.pids = []
188
175 189 if sys.platform == 'win32':
176 190 def _run_cmd(self):
177 191 # On Windows, use os.system instead of subprocess.call, because I
178 192 # was having problems with subprocess and I just don't know enough
179 193 # about win32 to debug this reliably. Os.system may be the 'old
180 194 # fashioned' way to do it, but it works just fine. If someone
181 195 # later can clean this up that's fine, as long as the tests run
182 196 # reliably in win32.
183 197 return os.system(' '.join(self.call_args))
184 198 else:
185 199 def _run_cmd(self):
186 return subprocess.call(self.call_args)
200 subp = subprocess.Popen(self.call_args)
201 self.pids.append(subp.pid)
202 # If this fails, the pid will be left in self.pids and cleaned up
203 # later, but if the wait call succeeds, then we can clear the
204 # stored pid.
205 retcode = subp.wait()
206 self.pids.pop()
207 return retcode
187 208
188 209 def run(self):
189 210 """Run the stored commands"""
190 211 try:
191 212 return self._run_cmd()
192 213 except:
193 214 import traceback
194 215 traceback.print_exc()
195 216 return 1 # signal failure
196 217
218 def __del__(self):
219 """Cleanup on exit by killing any leftover processes."""
220
221 if not hasattr(os, 'kill'):
222 return
223
224 for pid in self.pids:
225 try:
226 print 'Cleaning stale PID:', pid
227 os.kill(pid, signal.SIGKILL)
228 except OSError:
229 # This is just a best effort, if we fail or the process was
230 # really gone, ignore it.
231 pass
232
233
197 234
198 235 def make_runners():
199 236 """Define the top-level packages that need to be tested.
200 237 """
201 238
202 239 nose_packages = ['config', 'core', 'extensions', 'frontend', 'lib',
203 240 'scripts', 'testing', 'utils']
204 241 trial_packages = ['kernel']
205 242 #trial_packages = [] # dbg
206 243
207 244 if have_wx:
208 245 nose_packages.append('gui')
209 246
210 247 nose_packages = ['IPython.%s' % m for m in nose_packages ]
211 248 trial_packages = ['IPython.%s' % m for m in trial_packages ]
212 249
213 250 # Make runners, most with nose
214 251 nose_testers = [IPTester(params=v) for v in nose_packages]
215 252 runners = dict(zip(nose_packages, nose_testers))
216 253 # And add twisted ones if conditions are met
217 254 if have_zi and have_twisted and have_foolscap:
218 255 trial_testers = [IPTester('trial',params=v) for v in trial_packages]
219 256 runners.update(dict(zip(trial_packages,trial_testers)))
220 257
221 258 return runners
222 259
223 260
224 261 def run_iptest():
225 262 """Run the IPython test suite using nose.
226 263
227 264 This function is called when this script is **not** called with the form
228 265 `iptest all`. It simply calls nose with appropriate command line flags
229 266 and accepts all of the standard nose arguments.
230 267 """
231 268
232 269 warnings.filterwarnings('ignore',
233 270 'This will be removed soon. Use IPython.testing.util instead')
234 271
235 272 argv = sys.argv + [
236 273 # Loading ipdoctest causes problems with Twisted.
237 274 # I am removing this as a temporary fix to get the
238 275 # test suite back into working shape. Our nose
239 276 # plugin needs to be gone through with a fine
240 277 # toothed comb to find what is causing the problem.
241 278 # '--with-ipdoctest',
242 279 # '--ipdoctest-tests','--ipdoctest-extension=txt',
243 280 # '--detailed-errors',
244 281
245 282 # We add --exe because of setuptools' imbecility (it
246 283 # blindly does chmod +x on ALL files). Nose does the
247 284 # right thing and it tries to avoid executables,
248 285 # setuptools unfortunately forces our hand here. This
249 286 # has been discussed on the distutils list and the
250 287 # setuptools devs refuse to fix this problem!
251 288 '--exe',
252 289 ]
253 290
254 291 # Detect if any tests were required by explicitly calling an IPython
255 292 # submodule or giving a specific path
256 293 has_tests = False
257 294 for arg in sys.argv:
258 295 if 'IPython' in arg or arg.endswith('.py') or \
259 296 (':' in arg and '.py' in arg):
260 297 has_tests = True
261 298 break
262 299
263 300 # If nothing was specifically requested, test full IPython
264 301 if not has_tests:
265 302 argv.append('IPython')
266 303
267 304 # Construct list of plugins, omitting the existing doctest plugin, which
268 305 # ours replaces (and extends).
269 306 EXCLUDE = make_exclude()
270 307 plugins = []
271 308 # plugins = [IPythonDoctest(EXCLUDE)]
272 309 for p in nose.plugins.builtin.plugins:
273 310 plug = p()
274 311 if plug.name == 'doctest':
275 312 continue
276 313 plugins.append(plug)
277 314
278 315 TestProgram(argv=argv,plugins=plugins)
279 316
280 317
281 318 def run_iptestall():
282 319 """Run the entire IPython test suite by calling nose and trial.
283 320
284 321 This function constructs :class:`IPTester` instances for all IPython
285 322 modules and package and then runs each of them. This causes the modules
286 323 and packages of IPython to be tested each in their own subprocess using
287 324 nose or twisted.trial appropriately.
288 325 """
289 326
290 327 runners = make_runners()
291 328
292 329 # Run the test runners in a temporary dir so we can nuke it when finished
293 330 # to clean up any junk files left over by accident. This also makes it
294 331 # robust against being run in non-writeable directories by mistake, as the
295 332 # temp dir will always be user-writeable.
296 333 curdir = os.getcwd()
297 334 testdir = tempfile.gettempdir()
298 335 os.chdir(testdir)
299 336
300 337 # Run all test runners, tracking execution time
301 338 failed = {}
302 339 t_start = time.time()
303 340 try:
304 341 for name,runner in runners.iteritems():
305 342 print '*'*77
306 343 print 'IPython test group:',name
307 344 res = runner.run()
308 345 if res:
309 346 failed[name] = res
310 347 finally:
311 348 os.chdir(curdir)
312 349 t_end = time.time()
313 350 t_tests = t_end - t_start
314 351 nrunners = len(runners)
315 352 nfail = len(failed)
316 353 # summarize results
317 354 print
318 355 print '*'*77
319 356 print 'Ran %s test groups in %.3fs' % (nrunners, t_tests)
320 357 print
321 358 if not failed:
322 359 print 'OK'
323 360 else:
324 361 # If anything went wrong, point out what command to rerun manually to
325 362 # see the actual errors and individual summary
326 363 print 'ERROR - %s out of %s test groups failed.' % (nfail, nrunners)
327 364 for name in failed:
328 365 failed_runner = runners[name]
329 366 print '-'*40
330 367 print 'Runner failed:',name
331 368 print 'You may wish to rerun this one individually, with:'
332 369 print ' '.join(failed_runner.call_args)
333 370 print
334 371
335 372
336 373 def main():
337 374 if len(sys.argv) == 1:
338 375 run_iptestall()
339 376 else:
340 377 if sys.argv[1] == 'all':
341 378 run_iptestall()
342 379 else:
343 380 run_iptest()
344 381
345 382
346 383 if __name__ == '__main__':
347 384 main()
General Comments 0
You need to be logged in to leave comments. Login now