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