##// 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()
@@ -20,5 +20,5 b' Exiting."""'
20 import sys
20 import sys
21 print >> sys.stderr, error
21 print >> sys.stderr, error
22 else:
22 else:
23 from IPython.testing import iptest
23 from IPython.testing import iptestcontroller
24 iptest.main()
24 iptestcontroller.main()
@@ -30,13 +30,8 b' from __future__ import print_function'
30 import glob
30 import glob
31 import os
31 import os
32 import os.path as path
32 import os.path as path
33 import signal
34 import sys
33 import sys
35 import subprocess
36 import tempfile
37 import time
38 import warnings
34 import warnings
39 import multiprocessing.pool
40
35
41 # Now, proceed to import nose itself
36 # Now, proceed to import nose itself
42 import nose.plugins.builtin
37 import nose.plugins.builtin
@@ -45,12 +40,8 b' from nose import SkipTest'
45 from nose.core import TestProgram
40 from nose.core import TestProgram
46
41
47 # Our own imports
42 # Our own imports
48 from IPython.utils import py3compat
49 from IPython.utils.importstring import import_item
43 from IPython.utils.importstring import import_item
50 from IPython.utils.path import get_ipython_module_path, get_ipython_package_dir
44 from IPython.utils.path import 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
54 from IPython.utils.warn import warn
45 from IPython.utils.warn import warn
55
46
56 from IPython.testing import globalipapp
47 from IPython.testing import globalipapp
@@ -167,32 +158,6 b" have['zmq'] = test_for('zmq.pyzmq_version_info', min_zmq, callback=lambda x: x()"
167 # Functions and classes
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 def make_exclude():
162 def make_exclude():
198 """Make patterns of modules and packages to exclude from testing.
163 """Make patterns of modules and packages to exclude from testing.
@@ -325,160 +290,9 b' def make_exclude():'
325
290
326 return exclusions
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 special_test_suites = {
293 special_test_suites = {
443 'autoreload': ['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'],
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 def run_iptest():
298 def run_iptest():
@@ -546,111 +360,6 b' def run_iptest():'
546 # Now nose can run
360 # Now nose can run
547 TestProgram(argv=argv, addplugins=plugins)
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 if __name__ == '__main__':
364 if __name__ == '__main__':
656 main()
365 run_iptest()
General Comments 0
You need to be logged in to leave comments. Login now