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