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