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