##// END OF EJS Templates
Restore mention of number of test groups after success
Thomas Kluyver -
Show More
@@ -1,422 +1,422 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 brief_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 133 try:
134 134 return test_sections[self.section].will_run
135 135 except KeyError:
136 136 return True
137 137
138 138 def add_xunit(self):
139 139 xunit_file = os.path.abspath(self.section + '.xunit.xml')
140 140 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
141 141
142 142 def add_coverage(self):
143 143 try:
144 144 sources = test_sections[self.section].includes
145 145 except KeyError:
146 146 sources = ['IPython']
147 147
148 148 coverage_rc = ("[run]\n"
149 149 "data_file = {data_file}\n"
150 150 "source =\n"
151 151 " {source}\n"
152 152 ).format(data_file=os.path.abspath('.coverage.'+self.section),
153 153 source="\n ".join(sources))
154 154 config_file = os.path.join(self.workingdir.name, '.coveragerc')
155 155 with open(config_file, 'w') as f:
156 156 f.write(coverage_rc)
157 157
158 158 self.env['COVERAGE_PROCESS_START'] = config_file
159 159 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
160 160
161 161 def launch(self):
162 162 self.cmd[2] = self.pycmd
163 163 super(PyTestController, self).launch()
164 164
165 165
166 166 def prepare_py_test_controllers(inc_slow=False):
167 167 """Returns an ordered list of PyTestController instances to be run."""
168 168 to_run, not_run = [], []
169 169 if not inc_slow:
170 170 test_sections['parallel'].enabled = False
171 171
172 172 for name in test_group_names:
173 173 controller = PyTestController(name)
174 174 if controller.will_run:
175 175 to_run.append(controller)
176 176 else:
177 177 not_run.append(controller)
178 178 return to_run, not_run
179 179
180 180 def configure_controllers(controllers, xunit=False, coverage=False, extra_args=()):
181 181 """Apply options for a collection of TestController objects."""
182 182 for controller in controllers:
183 183 if xunit:
184 184 controller.add_xunit()
185 185 if coverage:
186 186 controller.add_coverage()
187 187 controller.cmd.extend(extra_args)
188 188
189 189 def do_run(controller):
190 190 try:
191 191 try:
192 192 controller.launch()
193 193 except Exception:
194 194 import traceback
195 195 traceback.print_exc()
196 196 return controller, 1 # signal failure
197 197
198 198 exitcode = controller.wait()
199 199 return controller, exitcode
200 200
201 201 except KeyboardInterrupt:
202 202 return controller, -signal.SIGINT
203 203 finally:
204 204 controller.cleanup()
205 205
206 206 def report():
207 207 """Return a string with a summary report of test-related variables."""
208 208
209 209 out = [ brief_sys_info(), '\n']
210 210
211 211 avail = []
212 212 not_avail = []
213 213
214 214 for k, is_avail in have.items():
215 215 if is_avail:
216 216 avail.append(k)
217 217 else:
218 218 not_avail.append(k)
219 219
220 220 if avail:
221 221 out.append('\nTools and libraries available at test time:\n')
222 222 avail.sort()
223 223 out.append(' ' + ' '.join(avail)+'\n')
224 224
225 225 if not_avail:
226 226 out.append('\nTools and libraries NOT available at test time:\n')
227 227 not_avail.sort()
228 228 out.append(' ' + ' '.join(not_avail)+'\n')
229 229
230 230 return ''.join(out)
231 231
232 232 def run_iptestall(options):
233 233 """Run the entire IPython test suite by calling nose and trial.
234 234
235 235 This function constructs :class:`IPTester` instances for all IPython
236 236 modules and package and then runs each of them. This causes the modules
237 237 and packages of IPython to be tested each in their own subprocess using
238 238 nose.
239 239
240 240 Parameters
241 241 ----------
242 242
243 243 All parameters are passed as attributes of the options object.
244 244
245 245 testgroups : list of str
246 246 Run only these sections of the test suite. If empty, run all the available
247 247 sections.
248 248
249 249 fast : int or None
250 250 Run the test suite in parallel, using n simultaneous processes. If None
251 251 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
252 252
253 253 inc_slow : bool
254 254 Include slow tests, like IPython.parallel. By default, these tests aren't
255 255 run.
256 256
257 257 xunit : bool
258 258 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
259 259
260 260 coverage : bool or str
261 261 Measure code coverage from tests. True will store the raw coverage data,
262 262 or pass 'html' or 'xml' to get reports.
263 263
264 264 extra_args : list
265 265 Extra arguments to pass to the test subprocesses, e.g. '-v'
266 266 """
267 267 if options.fast != 1:
268 268 # If running in parallel, capture output so it doesn't get interleaved
269 269 TestController.buffer_output = True
270 270
271 271 if options.testgroups:
272 272 to_run = [PyTestController(name) for name in options.testgroups]
273 273 not_run = []
274 274 else:
275 275 to_run, not_run = prepare_py_test_controllers(inc_slow=options.all)
276 276
277 277 configure_controllers(to_run, xunit=options.xunit, coverage=options.coverage,
278 278 extra_args=options.extra_args)
279 279
280 280 def justify(ltext, rtext, width=70, fill='-'):
281 281 ltext += ' '
282 282 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
283 283 return ltext + rtext
284 284
285 285 # Run all test runners, tracking execution time
286 286 failed = []
287 287 t_start = time.time()
288 288
289 289 print()
290 290 if options.fast == 1:
291 291 # This actually means sequential, i.e. with 1 job
292 292 for controller in to_run:
293 293 print('IPython test group:', controller.section)
294 294 sys.stdout.flush() # Show in correct order when output is piped
295 295 controller, res = do_run(controller)
296 296 if res:
297 297 failed.append(controller)
298 298 if res == -signal.SIGINT:
299 299 print("Interrupted")
300 300 break
301 301 print()
302 302
303 303 else:
304 304 # Run tests concurrently
305 305 try:
306 306 pool = multiprocessing.pool.ThreadPool(options.fast)
307 307 for (controller, res) in pool.imap_unordered(do_run, to_run):
308 308 res_string = 'OK' if res == 0 else 'FAILED'
309 309 print(justify('IPython test group: ' + controller.section, res_string))
310 310 if res:
311 311 print(bytes_to_str(controller.stdout))
312 312 failed.append(controller)
313 313 if res == -signal.SIGINT:
314 314 print("Interrupted")
315 315 break
316 316 except KeyboardInterrupt:
317 317 return
318 318
319 319 for controller in not_run:
320 320 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
321 321
322 322 t_end = time.time()
323 323 t_tests = t_end - t_start
324 324 nrunners = len(to_run)
325 325 nfail = len(failed)
326 326 # summarize results
327 327 print('_'*70)
328 328 print('Test suite completed for system with the following information:')
329 329 print(report())
330 330 took = "Took %.3fs." % t_tests
331 331 print('Status: ', end='')
332 332 if not failed:
333 print('OK.', took)
333 print('OK (%d test groups).' % nrunners, took)
334 334 else:
335 335 # If anything went wrong, point out what command to rerun manually to
336 336 # see the actual errors and individual summary
337 337 failed_sections = [c.section for c in failed]
338 338 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
339 339 nrunners, ', '.join(failed_sections)), took)
340 340 print()
341 341 print('You may wish to rerun these, with:')
342 342 print(' iptest', *failed_sections)
343 343 print()
344 344
345 345 if options.coverage:
346 346 from coverage import coverage
347 347 cov = coverage(data_file='.coverage')
348 348 cov.combine()
349 349 cov.save()
350 350
351 351 # Coverage HTML report
352 352 if options.coverage == 'html':
353 353 html_dir = 'ipy_htmlcov'
354 354 shutil.rmtree(html_dir, ignore_errors=True)
355 355 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
356 356 sys.stdout.flush()
357 357
358 358 # Custom HTML reporter to clean up module names.
359 359 from coverage.html import HtmlReporter
360 360 class CustomHtmlReporter(HtmlReporter):
361 361 def find_code_units(self, morfs):
362 362 super(CustomHtmlReporter, self).find_code_units(morfs)
363 363 for cu in self.code_units:
364 364 nameparts = cu.name.split(os.sep)
365 365 if 'IPython' not in nameparts:
366 366 continue
367 367 ix = nameparts.index('IPython')
368 368 cu.name = '.'.join(nameparts[ix:])
369 369
370 370 # Reimplement the html_report method with our custom reporter
371 371 cov._harvest_data()
372 372 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
373 373 html_title='IPython test coverage',
374 374 )
375 375 reporter = CustomHtmlReporter(cov, cov.config)
376 376 reporter.report(None)
377 377 print('done.')
378 378
379 379 # Coverage XML report
380 380 elif options.coverage == 'xml':
381 381 cov.xml_report(outfile='ipy_coverage.xml')
382 382
383 383 if failed:
384 384 # Ensure that our exit code indicates failure
385 385 sys.exit(1)
386 386
387 387
388 388 def main():
389 389 # Arguments after -- should be passed through to nose. Argparse treats
390 390 # everything after -- as regular positional arguments, so we separate them
391 391 # first.
392 392 try:
393 393 ix = sys.argv.index('--')
394 394 except ValueError:
395 395 to_parse = sys.argv[1:]
396 396 extra_args = []
397 397 else:
398 398 to_parse = sys.argv[1:ix]
399 399 extra_args = sys.argv[ix+1:]
400 400
401 401 parser = argparse.ArgumentParser(description='Run IPython test suite')
402 402 parser.add_argument('testgroups', nargs='*',
403 403 help='Run specified groups of tests. If omitted, run '
404 404 'all tests.')
405 405 parser.add_argument('--all', action='store_true',
406 406 help='Include slow tests not run by default.')
407 407 parser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
408 408 help='Run test sections in parallel.')
409 409 parser.add_argument('--xunit', action='store_true',
410 410 help='Produce Xunit XML results')
411 411 parser.add_argument('--coverage', nargs='?', const=True, default=False,
412 412 help="Measure test coverage. Specify 'html' or "
413 413 "'xml' to get reports.")
414 414
415 415 options = parser.parse_args(to_parse)
416 416 options.extra_args = extra_args
417 417
418 418 run_iptestall(options)
419 419
420 420
421 421 if __name__ == '__main__':
422 422 main()
General Comments 0
You need to be logged in to leave comments. Login now