##// END OF EJS Templates
Also show which test groups didn't run
Thomas Kluyver -
Show More
@@ -1,351 +1,362
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
2 """IPython Test Process Controller
3
3
4 This module runs one or more subprocesses which will actually run the IPython
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
5 test suite.
6
6
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2009-2011 The IPython Development Team
10 # Copyright (C) 2009-2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
19 from __future__ import print_function
20
20
21 import argparse
21 import argparse
22 import multiprocessing.pool
22 import multiprocessing.pool
23 import os
23 import os
24 import shutil
24 import shutil
25 import signal
25 import signal
26 import sys
26 import sys
27 import subprocess
27 import subprocess
28 import time
28 import time
29
29
30 from .iptest import have, test_group_names, test_sections
30 from .iptest import have, test_group_names, test_sections
31 from IPython.utils import py3compat
32 from IPython.utils.sysinfo import sys_info
31 from IPython.utils.sysinfo import sys_info
33 from IPython.utils.tempdir import TemporaryDirectory
32 from IPython.utils.tempdir import TemporaryDirectory
34
33
35
34
36 class IPTestController(object):
35 class IPTestController(object):
37 """Run iptest in a subprocess
36 """Run iptest in a subprocess
38 """
37 """
39 #: str, IPython test suite to be executed.
38 #: str, IPython test suite to be executed.
40 section = None
39 section = None
41 #: list, command line arguments to be executed
40 #: list, command line arguments to be executed
42 cmd = None
41 cmd = None
43 #: str, Python command to execute in subprocess
42 #: str, Python command to execute in subprocess
44 pycmd = None
43 pycmd = None
45 #: dict, extra environment variables to set for the subprocess
44 #: dict, extra environment variables to set for the subprocess
46 env = None
45 env = None
47 #: list, TemporaryDirectory instances to clear up when the process finishes
46 #: list, TemporaryDirectory instances to clear up when the process finishes
48 dirs = None
47 dirs = None
49 #: subprocess.Popen instance
48 #: subprocess.Popen instance
50 process = None
49 process = None
51 buffer_output = False
50 buffer_output = False
52
51
53 def __init__(self, section):
52 def __init__(self, section):
54 """Create new test runner."""
53 """Create new test runner."""
55 self.section = section
54 self.section = section
56 # pycmd is put into cmd[2] in IPTestController.launch()
55 # pycmd is put into cmd[2] in IPTestController.launch()
57 self.cmd = [sys.executable, '-c', None, section]
56 self.cmd = [sys.executable, '-c', None, section]
58 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
57 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
59 self.env = {}
58 self.env = {}
60 self.dirs = []
59 self.dirs = []
61 ipydir = TemporaryDirectory()
60 ipydir = TemporaryDirectory()
62 self.dirs.append(ipydir)
61 self.dirs.append(ipydir)
63 self.env['IPYTHONDIR'] = ipydir.name
62 self.env['IPYTHONDIR'] = ipydir.name
64 self.workingdir = workingdir = TemporaryDirectory()
63 self.workingdir = workingdir = TemporaryDirectory()
65 self.dirs.append(workingdir)
64 self.dirs.append(workingdir)
66 self.env['IPTEST_WORKING_DIR'] = workingdir.name
65 self.env['IPTEST_WORKING_DIR'] = workingdir.name
67 # This means we won't get odd effects from our own matplotlib config
66 # This means we won't get odd effects from our own matplotlib config
68 self.env['MPLCONFIGDIR'] = workingdir.name
67 self.env['MPLCONFIGDIR'] = workingdir.name
69
68
69 @property
70 def will_run(self):
71 return test_sections[self.section].will_run
72
70 def add_xunit(self):
73 def add_xunit(self):
71 xunit_file = os.path.abspath(self.section + '.xunit.xml')
74 xunit_file = os.path.abspath(self.section + '.xunit.xml')
72 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
75 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
73
76
74 def add_coverage(self):
77 def add_coverage(self):
75 coverage_rc = ("[run]\n"
78 coverage_rc = ("[run]\n"
76 "data_file = {data_file}\n"
79 "data_file = {data_file}\n"
77 "source =\n"
80 "source =\n"
78 " {source}\n"
81 " {source}\n"
79 ).format(data_file=os.path.abspath('.coverage.'+self.section),
82 ).format(data_file=os.path.abspath('.coverage.'+self.section),
80 source="\n ".join(test_sections[self.section].includes))
83 source="\n ".join(test_sections[self.section].includes))
81
84
82 config_file = os.path.join(self.workingdir.name, '.coveragerc')
85 config_file = os.path.join(self.workingdir.name, '.coveragerc')
83 with open(config_file, 'w') as f:
86 with open(config_file, 'w') as f:
84 f.write(coverage_rc)
87 f.write(coverage_rc)
85
88
86 self.env['COVERAGE_PROCESS_START'] = config_file
89 self.env['COVERAGE_PROCESS_START'] = config_file
87 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
90 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
88
91
89 def launch(self):
92 def launch(self):
90 # print('*** ENV:', self.env) # dbg
93 # print('*** ENV:', self.env) # dbg
91 # print('*** CMD:', self.cmd) # dbg
94 # print('*** CMD:', self.cmd) # dbg
92 env = os.environ.copy()
95 env = os.environ.copy()
93 env.update(self.env)
96 env.update(self.env)
94 output = subprocess.PIPE if self.buffer_output else None
97 output = subprocess.PIPE if self.buffer_output else None
95 stdout = subprocess.STDOUT if self.buffer_output else None
98 stdout = subprocess.STDOUT if self.buffer_output else None
96 self.cmd[2] = self.pycmd
99 self.cmd[2] = self.pycmd
97 self.process = subprocess.Popen(self.cmd, stdout=output,
100 self.process = subprocess.Popen(self.cmd, stdout=output,
98 stderr=stdout, env=env)
101 stderr=stdout, env=env)
99
102
100 def wait(self):
103 def wait(self):
101 self.stdout, _ = self.process.communicate()
104 self.stdout, _ = self.process.communicate()
102 return self.process.returncode
105 return self.process.returncode
103
106
104 def cleanup_process(self):
107 def cleanup_process(self):
105 """Cleanup on exit by killing any leftover processes."""
108 """Cleanup on exit by killing any leftover processes."""
106 subp = self.process
109 subp = self.process
107 if subp is None or (subp.poll() is not None):
110 if subp is None or (subp.poll() is not None):
108 return # Process doesn't exist, or is already dead.
111 return # Process doesn't exist, or is already dead.
109
112
110 try:
113 try:
111 print('Cleaning up stale PID: %d' % subp.pid)
114 print('Cleaning up stale PID: %d' % subp.pid)
112 subp.kill()
115 subp.kill()
113 except: # (OSError, WindowsError) ?
116 except: # (OSError, WindowsError) ?
114 # This is just a best effort, if we fail or the process was
117 # This is just a best effort, if we fail or the process was
115 # really gone, ignore it.
118 # really gone, ignore it.
116 pass
119 pass
117 else:
120 else:
118 for i in range(10):
121 for i in range(10):
119 if subp.poll() is None:
122 if subp.poll() is None:
120 time.sleep(0.1)
123 time.sleep(0.1)
121 else:
124 else:
122 break
125 break
123
126
124 if subp.poll() is None:
127 if subp.poll() is None:
125 # The process did not die...
128 # The process did not die...
126 print('... failed. Manual cleanup may be required.')
129 print('... failed. Manual cleanup may be required.')
127
130
128 def cleanup(self):
131 def cleanup(self):
129 "Kill process if it's still alive, and clean up temporary directories"
132 "Kill process if it's still alive, and clean up temporary directories"
130 self.cleanup_process()
133 self.cleanup_process()
131 for td in self.dirs:
134 for td in self.dirs:
132 td.cleanup()
135 td.cleanup()
133
136
134 __del__ = cleanup
137 __del__ = cleanup
135
138
136 def test_controllers_to_run(inc_slow=False, xunit=False, coverage=False):
139 def prepare_test_controllers(inc_slow=False, xunit=False, coverage=False):
137 """Returns an ordered list of IPTestController instances to be run."""
140 """Returns an ordered list of IPTestController instances to be run."""
138 res = []
141 to_run, not_run = [], []
139 if not inc_slow:
142 if not inc_slow:
140 test_sections['parallel'].enabled = False
143 test_sections['parallel'].enabled = False
141
144
142 for name in test_group_names:
145 for name in test_group_names:
143 if test_sections[name].will_run:
146 controller = IPTestController(name)
144 controller = IPTestController(name)
147 if xunit:
145 if xunit:
148 controller.add_xunit()
146 controller.add_xunit()
149 if coverage:
147 if coverage:
150 controller.add_coverage()
148 controller.add_coverage()
151 if controller.will_run:
149 res.append(controller)
152 to_run.append(controller)
150 return res
153 else:
154 not_run.append(controller)
155 return to_run, not_run
151
156
152 def do_run(controller):
157 def do_run(controller):
153 try:
158 try:
154 try:
159 try:
155 controller.launch()
160 controller.launch()
156 except Exception:
161 except Exception:
157 import traceback
162 import traceback
158 traceback.print_exc()
163 traceback.print_exc()
159 return controller, 1 # signal failure
164 return controller, 1 # signal failure
160
165
161 exitcode = controller.wait()
166 exitcode = controller.wait()
162 return controller, exitcode
167 return controller, exitcode
163
168
164 except KeyboardInterrupt:
169 except KeyboardInterrupt:
165 return controller, -signal.SIGINT
170 return controller, -signal.SIGINT
166 finally:
171 finally:
167 controller.cleanup()
172 controller.cleanup()
168
173
169 def report():
174 def report():
170 """Return a string with a summary report of test-related variables."""
175 """Return a string with a summary report of test-related variables."""
171
176
172 out = [ sys_info(), '\n']
177 out = [ sys_info(), '\n']
173
178
174 avail = []
179 avail = []
175 not_avail = []
180 not_avail = []
176
181
177 for k, is_avail in have.items():
182 for k, is_avail in have.items():
178 if is_avail:
183 if is_avail:
179 avail.append(k)
184 avail.append(k)
180 else:
185 else:
181 not_avail.append(k)
186 not_avail.append(k)
182
187
183 if avail:
188 if avail:
184 out.append('\nTools and libraries available at test time:\n')
189 out.append('\nTools and libraries available at test time:\n')
185 avail.sort()
190 avail.sort()
186 out.append(' ' + ' '.join(avail)+'\n')
191 out.append(' ' + ' '.join(avail)+'\n')
187
192
188 if not_avail:
193 if not_avail:
189 out.append('\nTools and libraries NOT available at test time:\n')
194 out.append('\nTools and libraries NOT available at test time:\n')
190 not_avail.sort()
195 not_avail.sort()
191 out.append(' ' + ' '.join(not_avail)+'\n')
196 out.append(' ' + ' '.join(not_avail)+'\n')
192
197
193 return ''.join(out)
198 return ''.join(out)
194
199
195 def run_iptestall(inc_slow=False, jobs=1, xunit_out=False, coverage_out=False):
200 def run_iptestall(inc_slow=False, jobs=1, xunit_out=False, coverage_out=False):
196 """Run the entire IPython test suite by calling nose and trial.
201 """Run the entire IPython test suite by calling nose and trial.
197
202
198 This function constructs :class:`IPTester` instances for all IPython
203 This function constructs :class:`IPTester` instances for all IPython
199 modules and package and then runs each of them. This causes the modules
204 modules and package and then runs each of them. This causes the modules
200 and packages of IPython to be tested each in their own subprocess using
205 and packages of IPython to be tested each in their own subprocess using
201 nose.
206 nose.
202
207
203 Parameters
208 Parameters
204 ----------
209 ----------
205
210
206 inc_slow : bool, optional
211 inc_slow : bool, optional
207 Include slow tests, like IPython.parallel. By default, these tests aren't
212 Include slow tests, like IPython.parallel. By default, these tests aren't
208 run.
213 run.
209
214
210 fast : bool, option
215 fast : bool, option
211 Run the test suite in parallel, if True, using as many threads as there
216 Run the test suite in parallel, if True, using as many threads as there
212 are processors
217 are processors
213 """
218 """
214 if jobs != 1:
219 if jobs != 1:
215 IPTestController.buffer_output = True
220 IPTestController.buffer_output = True
216
221
217 controllers = test_controllers_to_run(inc_slow=inc_slow, xunit=xunit_out,
222 to_run, not_run = prepare_test_controllers(inc_slow=inc_slow, xunit=xunit_out,
218 coverage=coverage_out)
223 coverage=coverage_out)
219
224
225 def justify(ltext, rtext, width=70, fill='-'):
226 ltext += ' '
227 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
228 return ltext + rtext
229
220 # Run all test runners, tracking execution time
230 # Run all test runners, tracking execution time
221 failed = []
231 failed = []
222 t_start = time.time()
232 t_start = time.time()
223
233
224 print('*'*70)
234 print('*'*70)
225 if jobs == 1:
235 if jobs == 1:
226 for controller in controllers:
236 for controller in to_run:
227 print('IPython test group:', controller.section)
237 print('IPython test group:', controller.section)
228 controller, res = do_run(controller)
238 controller, res = do_run(controller)
229 if res:
239 if res:
230 failed.append(controller)
240 failed.append(controller)
231 if res == -signal.SIGINT:
241 if res == -signal.SIGINT:
232 print("Interrupted")
242 print("Interrupted")
233 break
243 break
234 print()
244 print()
235
245
236 else:
246 else:
237 try:
247 try:
238 pool = multiprocessing.pool.ThreadPool(jobs)
248 pool = multiprocessing.pool.ThreadPool(jobs)
239 for (controller, res) in pool.imap_unordered(do_run, controllers):
249 for (controller, res) in pool.imap_unordered(do_run, to_run):
240 tgroup = 'IPython test group: ' + controller.section + ' '
250 res_string = 'OK' if res == 0 else 'FAILED'
241 res_string = ' OK' if res == 0 else ' FAILED'
251 print(justify('IPython test group: ' + controller.section, res_string))
242 res_string = res_string.rjust(70 - len(tgroup), '.')
243 print(tgroup + res_string)
244 if res:
252 if res:
245 print(controller.stdout)
253 print(controller.stdout)
246 failed.append(controller)
254 failed.append(controller)
247 if res == -signal.SIGINT:
255 if res == -signal.SIGINT:
248 print("Interrupted")
256 print("Interrupted")
249 break
257 break
250 except KeyboardInterrupt:
258 except KeyboardInterrupt:
251 return
259 return
252
260
261 for controller in not_run:
262 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
263
253 t_end = time.time()
264 t_end = time.time()
254 t_tests = t_end - t_start
265 t_tests = t_end - t_start
255 nrunners = len(controllers)
266 nrunners = len(to_run)
256 nfail = len(failed)
267 nfail = len(failed)
257 # summarize results
268 # summarize results
258 print('*'*70)
269 print('*'*70)
259 print('Test suite completed for system with the following information:')
270 print('Test suite completed for system with the following information:')
260 print(report())
271 print(report())
261 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
272 print('Ran %s test groups in %.3fs' % (nrunners, t_tests))
262 print()
273 print()
263 print('Status:')
274 print('Status:')
264 if not failed:
275 if not failed:
265 print('OK')
276 print('OK')
266 else:
277 else:
267 # If anything went wrong, point out what command to rerun manually to
278 # If anything went wrong, point out what command to rerun manually to
268 # see the actual errors and individual summary
279 # see the actual errors and individual summary
269 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
280 print('ERROR - %s out of %s test groups failed.' % (nfail, nrunners))
270 for controller in failed:
281 for controller in failed:
271 print('-'*40)
282 print('-'*40)
272 print('Runner failed:', controller.section)
283 print('Runner failed:', controller.section)
273 print('You may wish to rerun this one individually, with:')
284 print('You may wish to rerun this one individually, with:')
274 print(' iptest', *controller.cmd[3:])
285 print(' iptest', *controller.cmd[3:])
275 print()
286 print()
276
287
277 if coverage_out:
288 if coverage_out:
278 from coverage import coverage
289 from coverage import coverage
279 cov = coverage(data_file='.coverage')
290 cov = coverage(data_file='.coverage')
280 cov.combine()
291 cov.combine()
281 cov.save()
292 cov.save()
282
293
283 # Coverage HTML report
294 # Coverage HTML report
284 if coverage_out == 'html':
295 if coverage_out == 'html':
285 html_dir = 'ipy_htmlcov'
296 html_dir = 'ipy_htmlcov'
286 shutil.rmtree(html_dir, ignore_errors=True)
297 shutil.rmtree(html_dir, ignore_errors=True)
287 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
298 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
288 sys.stdout.flush()
299 sys.stdout.flush()
289
300
290 # Custom HTML reporter to clean up module names.
301 # Custom HTML reporter to clean up module names.
291 from coverage.html import HtmlReporter
302 from coverage.html import HtmlReporter
292 class CustomHtmlReporter(HtmlReporter):
303 class CustomHtmlReporter(HtmlReporter):
293 def find_code_units(self, morfs):
304 def find_code_units(self, morfs):
294 super(CustomHtmlReporter, self).find_code_units(morfs)
305 super(CustomHtmlReporter, self).find_code_units(morfs)
295 for cu in self.code_units:
306 for cu in self.code_units:
296 nameparts = cu.name.split(os.sep)
307 nameparts = cu.name.split(os.sep)
297 if 'IPython' not in nameparts:
308 if 'IPython' not in nameparts:
298 continue
309 continue
299 ix = nameparts.index('IPython')
310 ix = nameparts.index('IPython')
300 cu.name = '.'.join(nameparts[ix:])
311 cu.name = '.'.join(nameparts[ix:])
301
312
302 # Reimplement the html_report method with our custom reporter
313 # Reimplement the html_report method with our custom reporter
303 cov._harvest_data()
314 cov._harvest_data()
304 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
315 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
305 html_title='IPython test coverage',
316 html_title='IPython test coverage',
306 )
317 )
307 reporter = CustomHtmlReporter(cov, cov.config)
318 reporter = CustomHtmlReporter(cov, cov.config)
308 reporter.report(None)
319 reporter.report(None)
309 print('done.')
320 print('done.')
310
321
311 # Coverage XML report
322 # Coverage XML report
312 elif coverage_out == 'xml':
323 elif coverage_out == 'xml':
313 cov.xml_report(outfile='ipy_coverage.xml')
324 cov.xml_report(outfile='ipy_coverage.xml')
314
325
315 if failed:
326 if failed:
316 # Ensure that our exit code indicates failure
327 # Ensure that our exit code indicates failure
317 sys.exit(1)
328 sys.exit(1)
318
329
319
330
320 def main():
331 def main():
321 if len(sys.argv) > 1 and (sys.argv[1] in test_sections):
332 if len(sys.argv) > 1 and (sys.argv[1] in test_sections):
322 from .iptest import run_iptest
333 from .iptest import run_iptest
323 # This is in-process
334 # This is in-process
324 run_iptest()
335 run_iptest()
325 return
336 return
326
337
327 parser = argparse.ArgumentParser(description='Run IPython test suite')
338 parser = argparse.ArgumentParser(description='Run IPython test suite')
328 parser.add_argument('--all', action='store_true',
339 parser.add_argument('--all', action='store_true',
329 help='Include slow tests not run by default.')
340 help='Include slow tests not run by default.')
330 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
341 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1,
331 help='Run test sections in parallel.')
342 help='Run test sections in parallel.')
332 parser.add_argument('--xunit', action='store_true',
343 parser.add_argument('--xunit', action='store_true',
333 help='Produce Xunit XML results')
344 help='Produce Xunit XML results')
334 parser.add_argument('--coverage', nargs='?', const=True, default=False,
345 parser.add_argument('--coverage', nargs='?', const=True, default=False,
335 help="Measure test coverage. Specify 'html' or "
346 help="Measure test coverage. Specify 'html' or "
336 "'xml' to get reports.")
347 "'xml' to get reports.")
337
348
338 options = parser.parse_args()
349 options = parser.parse_args()
339
350
340 try:
351 try:
341 jobs = int(options.fast)
352 jobs = int(options.fast)
342 except TypeError:
353 except TypeError:
343 jobs = options.fast
354 jobs = options.fast
344
355
345 # This starts subprocesses
356 # This starts subprocesses
346 run_iptestall(inc_slow=options.all, jobs=jobs,
357 run_iptestall(inc_slow=options.all, jobs=jobs,
347 xunit_out=options.xunit, coverage_out=options.coverage)
358 xunit_out=options.xunit, coverage_out=options.coverage)
348
359
349
360
350 if __name__ == '__main__':
361 if __name__ == '__main__':
351 main()
362 main()
General Comments 0
You need to be logged in to leave comments. Login now