##// END OF EJS Templates
Produce coverage xml reports from subprocess test runners.
Thomas Kluyver -
Show More
@@ -1,485 +1,496 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
28 28 # Stdlib
29 29 import os
30 30 import os.path as path
31 31 import signal
32 32 import sys
33 33 import subprocess
34 34 import tempfile
35 35 import time
36 36 import warnings
37 37
38 38 # Note: monkeypatch!
39 39 # We need to monkeypatch a small problem in nose itself first, before importing
40 40 # it for actual use. This should get into nose upstream, but its release cycle
41 41 # is slow and we need it for our parametric tests to work correctly.
42 42 from IPython.testing import nosepatch
43 43 # Now, proceed to import nose itself
44 44 import nose.plugins.builtin
45 45 from nose.core import TestProgram
46 46
47 47 # Our own imports
48 48 from IPython.utils.importstring import import_item
49 49 from IPython.utils.path import get_ipython_module_path
50 50 from IPython.utils.process import find_cmd, pycmd2argv
51 51 from IPython.utils.sysinfo import sys_info
52 52
53 53 from IPython.testing import globalipapp
54 54 from IPython.testing.plugin.ipdoctest import IPythonDoctest
55 55 from IPython.external.decorators import KnownFailure
56 56
57 57 pjoin = path.join
58 58
59 59
60 60 #-----------------------------------------------------------------------------
61 61 # Globals
62 62 #-----------------------------------------------------------------------------
63 63
64 64
65 65 #-----------------------------------------------------------------------------
66 66 # Warnings control
67 67 #-----------------------------------------------------------------------------
68 68
69 69 # Twisted generates annoying warnings with Python 2.6, as will do other code
70 70 # that imports 'sets' as of today
71 71 warnings.filterwarnings('ignore', 'the sets module is deprecated',
72 72 DeprecationWarning )
73 73
74 74 # This one also comes from Twisted
75 75 warnings.filterwarnings('ignore', 'the sha module is deprecated',
76 76 DeprecationWarning)
77 77
78 78 # Wx on Fedora11 spits these out
79 79 warnings.filterwarnings('ignore', 'wxPython/wxWidgets release number mismatch',
80 80 UserWarning)
81 81
82 82 #-----------------------------------------------------------------------------
83 83 # Logic for skipping doctests
84 84 #-----------------------------------------------------------------------------
85 85 def extract_version(mod):
86 86 return mod.__version__
87 87
88 88 def test_for(item, min_version=None, callback=extract_version):
89 89 """Test to see if item is importable, and optionally check against a minimum
90 90 version.
91 91
92 92 If min_version is given, the default behavior is to check against the
93 93 `__version__` attribute of the item, but specifying `callback` allows you to
94 94 extract the value you are interested in. e.g::
95 95
96 96 In [1]: import sys
97 97
98 98 In [2]: from IPython.testing.iptest import test_for
99 99
100 100 In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
101 101 Out[3]: True
102 102
103 103 """
104 104 try:
105 105 check = import_item(item)
106 106 except (ImportError, RuntimeError):
107 107 # GTK reports Runtime error if it can't be initialized even if it's
108 108 # importable.
109 109 return False
110 110 else:
111 111 if min_version:
112 112 if callback:
113 113 # extra processing step to get version to compare
114 114 check = callback(check)
115 115
116 116 return check >= min_version
117 117 else:
118 118 return True
119 119
120 120 # Global dict where we can store information on what we have and what we don't
121 121 # have available at test run time
122 122 have = {}
123 123
124 124 have['curses'] = test_for('_curses')
125 125 have['matplotlib'] = test_for('matplotlib')
126 126 have['pexpect'] = test_for('IPython.external.pexpect')
127 127 have['pymongo'] = test_for('pymongo')
128 128 have['wx'] = test_for('wx')
129 129 have['wx.aui'] = test_for('wx.aui')
130 130 have['qt'] = test_for('IPython.external.qt')
131 131 have['sqlite3'] = test_for('sqlite3')
132 132
133 133 have['tornado'] = test_for('tornado.version_info', (2,1,0), callback=None)
134 134
135 135 if os.name == 'nt':
136 136 min_zmq = (2,1,7)
137 137 else:
138 138 min_zmq = (2,1,4)
139 139
140 140 def version_tuple(mod):
141 141 "turn '2.1.9' into (2,1,9), and '2.1dev' into (2,1,999)"
142 142 # turn 'dev' into 999, because Python3 rejects str-int comparisons
143 143 vs = mod.__version__.replace('dev', '.999')
144 144 tup = tuple([int(v) for v in vs.split('.') ])
145 145 return tup
146 146
147 147 have['zmq'] = test_for('zmq', min_zmq, version_tuple)
148 148
149 149 #-----------------------------------------------------------------------------
150 150 # Functions and classes
151 151 #-----------------------------------------------------------------------------
152 152
153 153 def report():
154 154 """Return a string with a summary report of test-related variables."""
155 155
156 156 out = [ sys_info(), '\n']
157 157
158 158 avail = []
159 159 not_avail = []
160 160
161 161 for k, is_avail in have.items():
162 162 if is_avail:
163 163 avail.append(k)
164 164 else:
165 165 not_avail.append(k)
166 166
167 167 if avail:
168 168 out.append('\nTools and libraries available at test time:\n')
169 169 avail.sort()
170 170 out.append(' ' + ' '.join(avail)+'\n')
171 171
172 172 if not_avail:
173 173 out.append('\nTools and libraries NOT available at test time:\n')
174 174 not_avail.sort()
175 175 out.append(' ' + ' '.join(not_avail)+'\n')
176 176
177 177 return ''.join(out)
178 178
179 179
180 180 def make_exclude():
181 181 """Make patterns of modules and packages to exclude from testing.
182 182
183 183 For the IPythonDoctest plugin, we need to exclude certain patterns that
184 184 cause testing problems. We should strive to minimize the number of
185 185 skipped modules, since this means untested code.
186 186
187 187 These modules and packages will NOT get scanned by nose at all for tests.
188 188 """
189 189 # Simple utility to make IPython paths more readably, we need a lot of
190 190 # these below
191 191 ipjoin = lambda *paths: pjoin('IPython', *paths)
192 192
193 193 exclusions = [ipjoin('external'),
194 194 pjoin('IPython_doctest_plugin'),
195 195 ipjoin('quarantine'),
196 196 ipjoin('deathrow'),
197 197 ipjoin('testing', 'attic'),
198 198 # This guy is probably attic material
199 199 ipjoin('testing', 'mkdoctests'),
200 200 # Testing inputhook will need a lot of thought, to figure out
201 201 # how to have tests that don't lock up with the gui event
202 202 # loops in the picture
203 203 ipjoin('lib', 'inputhook'),
204 204 # Config files aren't really importable stand-alone
205 205 ipjoin('config', 'default'),
206 206 ipjoin('config', 'profile'),
207 207 ]
208 208 if not have['sqlite3']:
209 209 exclusions.append(ipjoin('core', 'tests', 'test_history'))
210 210 exclusions.append(ipjoin('core', 'history'))
211 211 if not have['wx']:
212 212 exclusions.append(ipjoin('lib', 'inputhookwx'))
213 213
214 214 # We do this unconditionally, so that the test suite doesn't import
215 215 # gtk, changing the default encoding and masking some unicode bugs.
216 216 exclusions.append(ipjoin('lib', 'inputhookgtk'))
217 217
218 218 # These have to be skipped on win32 because the use echo, rm, cd, etc.
219 219 # See ticket https://github.com/ipython/ipython/issues/87
220 220 if sys.platform == 'win32':
221 221 exclusions.append(ipjoin('testing', 'plugin', 'test_exampleip'))
222 222 exclusions.append(ipjoin('testing', 'plugin', 'dtexample'))
223 223
224 224 if not have['pexpect']:
225 225 exclusions.extend([ipjoin('scripts', 'irunner'),
226 226 ipjoin('lib', 'irunner'),
227 227 ipjoin('lib', 'tests', 'test_irunner'),
228 228 ipjoin('frontend', 'terminal', 'console'),
229 229 ])
230 230
231 231 if not have['zmq']:
232 232 exclusions.append(ipjoin('zmq'))
233 233 exclusions.append(ipjoin('frontend', 'qt'))
234 234 exclusions.append(ipjoin('frontend', 'html'))
235 235 exclusions.append(ipjoin('frontend', 'consoleapp.py'))
236 236 exclusions.append(ipjoin('frontend', 'terminal', 'console'))
237 237 exclusions.append(ipjoin('parallel'))
238 238 elif not have['qt']:
239 239 exclusions.append(ipjoin('frontend', 'qt'))
240 240
241 241 if not have['pymongo']:
242 242 exclusions.append(ipjoin('parallel', 'controller', 'mongodb'))
243 243 exclusions.append(ipjoin('parallel', 'tests', 'test_mongodb'))
244 244
245 245 if not have['matplotlib']:
246 246 exclusions.extend([ipjoin('core', 'pylabtools'),
247 247 ipjoin('core', 'tests', 'test_pylabtools')])
248 248
249 249 if not have['tornado']:
250 250 exclusions.append(ipjoin('frontend', 'html'))
251 251
252 252 # This is needed for the reg-exp to match on win32 in the ipdoctest plugin.
253 253 if sys.platform == 'win32':
254 254 exclusions = [s.replace('\\','\\\\') for s in exclusions]
255 255
256 256 return exclusions
257 257
258 258
259 259 class IPTester(object):
260 260 """Call that calls iptest or trial in a subprocess.
261 261 """
262 262 #: string, name of test runner that will be called
263 263 runner = None
264 264 #: list, parameters for test runner
265 265 params = None
266 266 #: list, arguments of system call to be made to call test runner
267 267 call_args = None
268 268 #: list, process ids of subprocesses we start (for cleanup)
269 269 pids = None
270 #: str, coverage xml output file
271 coverage_xml = None
270 272
271 273 def __init__(self, runner='iptest', params=None):
272 274 """Create new test runner."""
273 275 p = os.path
274 276 if runner == 'iptest':
275 277 iptest_app = get_ipython_module_path('IPython.testing.iptest')
276 278 self.runner = pycmd2argv(iptest_app) + sys.argv[1:]
277 279 else:
278 280 raise Exception('Not a valid test runner: %s' % repr(runner))
279 281 if params is None:
280 282 params = []
281 283 if isinstance(params, str):
282 284 params = [params]
283 285 self.params = params
284 286
285 287 # Assemble call
286 288 self.call_args = self.runner+self.params
287 289
290 sect = [p for p in self.params if p.startswith('IPython')][0]
288 291 if '--with-xunit' in self.call_args:
289 sect = [p for p in self.params if p.startswith('IPython')][0]
290 292 self.call_args.append('--xunit-file=%s' % path.abspath(sect+'.xunit.xml'))
293
294 if '--with-coverage' in self.call_args:
295 self.coverage_xml = path.abspath(sect+".coverage.xml")
296 self.call_args.remove('--with-coverage')
297 self.call_args = ["python-coverage", "run", "--source="+sect] + self.call_args[1:]
291 298
292 299 # Store pids of anything we start to clean up on deletion, if possible
293 300 # (on posix only, since win32 has no os.kill)
294 301 self.pids = []
295 302
296 303 if sys.platform == 'win32':
297 304 def _run_cmd(self):
298 305 # On Windows, use os.system instead of subprocess.call, because I
299 306 # was having problems with subprocess and I just don't know enough
300 307 # about win32 to debug this reliably. Os.system may be the 'old
301 308 # fashioned' way to do it, but it works just fine. If someone
302 309 # later can clean this up that's fine, as long as the tests run
303 310 # reliably in win32.
304 311 # What types of problems are you having. They may be related to
305 312 # running Python in unboffered mode. BG.
306 313 return os.system(' '.join(self.call_args))
307 314 else:
308 315 def _run_cmd(self):
309 316 # print >> sys.stderr, '*** CMD:', ' '.join(self.call_args) # dbg
310 317 subp = subprocess.Popen(self.call_args)
311 318 self.pids.append(subp.pid)
312 319 # If this fails, the pid will be left in self.pids and cleaned up
313 320 # later, but if the wait call succeeds, then we can clear the
314 321 # stored pid.
315 322 retcode = subp.wait()
316 323 self.pids.pop()
317 324 return retcode
318 325
319 326 def run(self):
320 327 """Run the stored commands"""
321 328 try:
322 return self._run_cmd()
329 retcode = self._run_cmd()
323 330 except:
324 331 import traceback
325 332 traceback.print_exc()
326 333 return 1 # signal failure
334
335 if self.coverage_xml:
336 subprocess.check_call(["python-coverage", "xml", "-o", self.coverage_xml])
337 return retcode
327 338
328 339 def __del__(self):
329 340 """Cleanup on exit by killing any leftover processes."""
330 341
331 342 if not hasattr(os, 'kill'):
332 343 return
333 344
334 345 for pid in self.pids:
335 346 try:
336 347 print 'Cleaning stale PID:', pid
337 348 os.kill(pid, signal.SIGKILL)
338 349 except OSError:
339 350 # This is just a best effort, if we fail or the process was
340 351 # really gone, ignore it.
341 352 pass
342 353
343 354
344 355 def make_runners():
345 356 """Define the top-level packages that need to be tested.
346 357 """
347 358
348 359 # Packages to be tested via nose, that only depend on the stdlib
349 360 nose_pkg_names = ['config', 'core', 'extensions', 'frontend', 'lib',
350 361 'scripts', 'testing', 'utils', 'nbformat' ]
351 362
352 363 if have['zmq']:
353 364 nose_pkg_names.append('parallel')
354 365
355 366 # For debugging this code, only load quick stuff
356 367 #nose_pkg_names = ['core', 'extensions'] # dbg
357 368
358 369 # Make fully qualified package names prepending 'IPython.' to our name lists
359 370 nose_packages = ['IPython.%s' % m for m in nose_pkg_names ]
360 371
361 372 # Make runners
362 373 runners = [ (v, IPTester('iptest', params=v)) for v in nose_packages ]
363 374
364 375 return runners
365 376
366 377
367 378 def run_iptest():
368 379 """Run the IPython test suite using nose.
369 380
370 381 This function is called when this script is **not** called with the form
371 382 `iptest all`. It simply calls nose with appropriate command line flags
372 383 and accepts all of the standard nose arguments.
373 384 """
374 385
375 386 warnings.filterwarnings('ignore',
376 387 'This will be removed soon. Use IPython.testing.util instead')
377 388
378 389 argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
379 390
380 391 # Loading ipdoctest causes problems with Twisted, but
381 392 # our test suite runner now separates things and runs
382 393 # all Twisted tests with trial.
383 394 '--with-ipdoctest',
384 395 '--ipdoctest-tests','--ipdoctest-extension=txt',
385 396
386 397 # We add --exe because of setuptools' imbecility (it
387 398 # blindly does chmod +x on ALL files). Nose does the
388 399 # right thing and it tries to avoid executables,
389 400 # setuptools unfortunately forces our hand here. This
390 401 # has been discussed on the distutils list and the
391 402 # setuptools devs refuse to fix this problem!
392 403 '--exe',
393 404 ]
394 405
395 406 if nose.__version__ >= '0.11':
396 407 # I don't fully understand why we need this one, but depending on what
397 408 # directory the test suite is run from, if we don't give it, 0 tests
398 409 # get run. Specifically, if the test suite is run from the source dir
399 410 # with an argument (like 'iptest.py IPython.core', 0 tests are run,
400 411 # even if the same call done in this directory works fine). It appears
401 412 # that if the requested package is in the current dir, nose bails early
402 413 # by default. Since it's otherwise harmless, leave it in by default
403 414 # for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
404 415 argv.append('--traverse-namespace')
405 416
406 417 # use our plugin for doctesting. It will remove the standard doctest plugin
407 418 # if it finds it enabled
408 419 plugins = [IPythonDoctest(make_exclude()), KnownFailure()]
409 420 # We need a global ipython running in this process
410 421 globalipapp.start_ipython()
411 422 # Now nose can run
412 423 TestProgram(argv=argv, addplugins=plugins)
413 424
414 425
415 426 def run_iptestall():
416 427 """Run the entire IPython test suite by calling nose and trial.
417 428
418 429 This function constructs :class:`IPTester` instances for all IPython
419 430 modules and package and then runs each of them. This causes the modules
420 431 and packages of IPython to be tested each in their own subprocess using
421 432 nose or twisted.trial appropriately.
422 433 """
423 434
424 435 runners = make_runners()
425 436
426 437 # Run the test runners in a temporary dir so we can nuke it when finished
427 438 # to clean up any junk files left over by accident. This also makes it
428 439 # robust against being run in non-writeable directories by mistake, as the
429 440 # temp dir will always be user-writeable.
430 441 curdir = os.getcwdu()
431 442 testdir = tempfile.gettempdir()
432 443 os.chdir(testdir)
433 444
434 445 # Run all test runners, tracking execution time
435 446 failed = []
436 447 t_start = time.time()
437 448 try:
438 449 for (name, runner) in runners:
439 450 print '*'*70
440 451 print 'IPython test group:',name
441 452 res = runner.run()
442 453 if res:
443 454 failed.append( (name, runner) )
444 455 finally:
445 456 os.chdir(curdir)
446 457 t_end = time.time()
447 458 t_tests = t_end - t_start
448 459 nrunners = len(runners)
449 460 nfail = len(failed)
450 461 # summarize results
451 462 print
452 463 print '*'*70
453 464 print 'Test suite completed for system with the following information:'
454 465 print report()
455 466 print 'Ran %s test groups in %.3fs' % (nrunners, t_tests)
456 467 print
457 468 print 'Status:'
458 469 if not failed:
459 470 print 'OK'
460 471 else:
461 472 # If anything went wrong, point out what command to rerun manually to
462 473 # see the actual errors and individual summary
463 474 print 'ERROR - %s out of %s test groups failed.' % (nfail, nrunners)
464 475 for name, failed_runner in failed:
465 476 print '-'*40
466 477 print 'Runner failed:',name
467 478 print 'You may wish to rerun this one individually, with:'
468 479 print ' '.join(failed_runner.call_args)
469 480 print
470 481 # Ensure that our exit code indicates failure
471 482 sys.exit(1)
472 483
473 484
474 485 def main():
475 486 for arg in sys.argv[1:]:
476 487 if arg.startswith('IPython'):
477 488 # This is in-process
478 489 run_iptest()
479 490 else:
480 491 # This starts subprocesses
481 492 run_iptestall()
482 493
483 494
484 495 if __name__ == '__main__':
485 496 main()
General Comments 0
You need to be logged in to leave comments. Login now