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