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