##// END OF EJS Templates
don't change name of section
Paul Ivanov -
Show More
@@ -1,520 +1,520 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 from multiprocessing import Process, Queue
24 24 import os
25 25 import shutil
26 26 import signal
27 27 import sys
28 28 import subprocess
29 29 import time
30 30
31 31 from .iptest import have, test_group_names as py_test_group_names, test_sections
32 32 from IPython.utils.path import compress_user
33 33 from IPython.utils.py3compat import bytes_to_str
34 34 from IPython.utils.sysinfo import get_sys_info
35 35 from IPython.utils.tempdir import TemporaryDirectory
36 36
37 37
38 38 class TestController(object):
39 39 """Run tests in a subprocess
40 40 """
41 41 #: str, IPython test suite to be executed.
42 42 section = None
43 43 #: list, command line arguments to be executed
44 44 cmd = None
45 45 #: dict, extra environment variables to set for the subprocess
46 46 env = None
47 47 #: list, TemporaryDirectory instances to clear up when the process finishes
48 48 dirs = None
49 49 #: subprocess.Popen instance
50 50 process = None
51 51 #: str, process stdout+stderr
52 52 stdout = None
53 53 #: bool, whether to capture process stdout & stderr
54 54 buffer_output = False
55 55
56 56 def __init__(self):
57 57 self.cmd = []
58 58 self.env = {}
59 59 self.dirs = []
60 60
61 61 def launch(self):
62 62 # print('*** ENV:', self.env) # dbg
63 63 # print('*** CMD:', self.cmd) # dbg
64 64 env = os.environ.copy()
65 65 env.update(self.env)
66 66 output = subprocess.PIPE if self.buffer_output else None
67 67 stdout = subprocess.STDOUT if self.buffer_output else None
68 68 self.process = subprocess.Popen(self.cmd, stdout=output,
69 69 stderr=stdout, env=env)
70 70
71 71 def wait(self):
72 72 self.stdout, _ = self.process.communicate()
73 73 return self.process.returncode
74 74
75 75 def cleanup_process(self):
76 76 """Cleanup on exit by killing any leftover processes."""
77 77 subp = self.process
78 78 if subp is None or (subp.poll() is not None):
79 79 return # Process doesn't exist, or is already dead.
80 80
81 81 try:
82 82 print('Cleaning up stale PID: %d' % subp.pid)
83 83 subp.kill()
84 84 except: # (OSError, WindowsError) ?
85 85 # This is just a best effort, if we fail or the process was
86 86 # really gone, ignore it.
87 87 pass
88 88 else:
89 89 for i in range(10):
90 90 if subp.poll() is None:
91 91 time.sleep(0.1)
92 92 else:
93 93 break
94 94
95 95 if subp.poll() is None:
96 96 # The process did not die...
97 97 print('... failed. Manual cleanup may be required.')
98 98
99 99 def cleanup(self):
100 100 "Kill process if it's still alive, and clean up temporary directories"
101 101 self.cleanup_process()
102 102 for td in self.dirs:
103 103 td.cleanup()
104 104
105 105 __del__ = cleanup
106 106
107 107 class PyTestController(TestController):
108 108 """Run Python tests using IPython.testing.iptest"""
109 109 #: str, Python command to execute in subprocess
110 110 pycmd = None
111 111
112 112 def __init__(self, section):
113 113 """Create new test runner."""
114 114 TestController.__init__(self)
115 115 self.section = section
116 116 # pycmd is put into cmd[2] in PyTestController.launch()
117 117 self.cmd = [sys.executable, '-c', None, section]
118 118 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
119 119 ipydir = TemporaryDirectory()
120 120 self.dirs.append(ipydir)
121 121 self.env['IPYTHONDIR'] = ipydir.name
122 122 self.workingdir = workingdir = TemporaryDirectory()
123 123 self.dirs.append(workingdir)
124 124 self.env['IPTEST_WORKING_DIR'] = workingdir.name
125 125 # This means we won't get odd effects from our own matplotlib config
126 126 self.env['MPLCONFIGDIR'] = workingdir.name
127 127
128 128 @property
129 129 def will_run(self):
130 130 try:
131 131 return test_sections[self.section].will_run
132 132 except KeyError:
133 133 return True
134 134
135 135 def add_xunit(self):
136 136 xunit_file = os.path.abspath(self.section + '.xunit.xml')
137 137 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
138 138
139 139 def add_coverage(self):
140 140 try:
141 141 sources = test_sections[self.section].includes
142 142 except KeyError:
143 143 sources = ['IPython']
144 144
145 145 coverage_rc = ("[run]\n"
146 146 "data_file = {data_file}\n"
147 147 "source =\n"
148 148 " {source}\n"
149 149 ).format(data_file=os.path.abspath('.coverage.'+self.section),
150 150 source="\n ".join(sources))
151 151 config_file = os.path.join(self.workingdir.name, '.coveragerc')
152 152 with open(config_file, 'w') as f:
153 153 f.write(coverage_rc)
154 154
155 155 self.env['COVERAGE_PROCESS_START'] = config_file
156 156 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
157 157
158 158 def launch(self):
159 159 self.cmd[2] = self.pycmd
160 160 super(PyTestController, self).launch()
161 161
162 162 js_prefix = 'js/'
163 163
164 164 def get_js_test_dir():
165 165 import IPython.html.tests as t
166 166 return os.path.join(os.path.dirname(t.__file__), 'casperjs','')
167 167
168 168 def all_js_groups():
169 169 import glob
170 170 test_dir = get_js_test_dir()
171 171 all_subdirs = glob.glob(test_dir + '*/')
172 172 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs]
173 173
174 174 class JSController(TestController):
175 175 """Run CasperJS tests """
176 176 def __init__(self, section):
177 177 """Create new test runner."""
178 178 TestController.__init__(self)
179 self.section = section[len(js_prefix):]
179 self.section = section
180 180
181 181 self.ipydir = TemporaryDirectory()
182 182 self.nbdir = TemporaryDirectory()
183 183 print("Running notebook tests in directory: %r" % self.nbdir.name)
184 184 os.makedirs(os.path.join(self.nbdir.name, os.path.join('subdir1', 'subdir1a')))
185 185 os.makedirs(os.path.join(self.nbdir.name, os.path.join('subdir2', 'subdir2a')))
186 186 self.dirs.append(self.ipydir)
187 187 self.dirs.append(self.nbdir)
188 188
189 189 def launch(self):
190 190 # start the ipython notebook, so we get the port number
191 191 self._init_server()
192 192 js_test_dir = get_js_test_dir()
193 193 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
194 test_cases = os.path.join(js_test_dir, self.section)
194 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
195 195 port = '--port=' + str(self.server_port)
196 196 self.cmd = ['casperjs', 'test', port, includes, test_cases]
197 197 super(JSController, self).launch()
198 198
199 199 @property
200 200 def will_run(self):
201 201 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
202 202
203 203 def _init_server(self):
204 204 "Start the notebook server in a separate process"
205 205 self.queue = q = Queue()
206 206 self.server = Process(target=run_webapp, args=(q, self.ipydir.name, self.nbdir.name))
207 207 self.server.start()
208 208 self.server_port = q.get()
209 209
210 210 def cleanup(self):
211 211 self.server.terminate()
212 212 self.server.join()
213 213 TestController.cleanup(self)
214 214
215 215 def run_webapp(q, ipydir, nbdir, loglevel=0):
216 216 """start the IPython Notebook, and pass port back to the queue"""
217 217 import os
218 218 import IPython.html.notebookapp as nbapp
219 219 import sys
220 220 sys.stderr = open(os.devnull, 'w')
221 221 server = nbapp.NotebookApp()
222 222 args = ['--no-browser']
223 223 args.extend(['--ipython-dir', ipydir])
224 224 args.extend(['--notebook-dir', nbdir])
225 225 args.extend(['--log-level', str(loglevel)])
226 226 server.initialize(args)
227 227 # communicate the port number to the parent process
228 228 q.put(server.port)
229 229 server.start()
230 230
231 231 def prepare_controllers(options):
232 232 """Returns two lists of TestController instances, those to run, and those
233 233 not to run."""
234 234 testgroups = options.testgroups
235 235
236 236 if testgroups:
237 237 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
238 238 or g.startswith('IPython.')]
239 239 if 'js' in testgroups:
240 240 js_testgroups = all_js_groups()
241 241 else:
242 242 js_testgroups = [g for g in testgroups if g not in py_testgroups]
243 243 else:
244 244 py_testgroups = py_test_group_names
245 245 js_testgroups = all_js_groups()
246 246 if not options.all:
247 247 test_sections['parallel'].enabled = False
248 248
249 249 c_js = [JSController(name) for name in js_testgroups]
250 250 c_py = [PyTestController(name) for name in py_testgroups]
251 251
252 252 configure_py_controllers(c_py, xunit=options.xunit,
253 253 coverage=options.coverage, subproc_streams=options.subproc_streams,
254 254 extra_args=options.extra_args)
255 255
256 256 controllers = c_py + c_js
257 257 to_run = [c for c in controllers if c.will_run]
258 258 not_run = [c for c in controllers if not c.will_run]
259 259 return to_run, not_run
260 260
261 261 def configure_py_controllers(controllers, xunit=False, coverage=False,
262 262 subproc_streams='capture', extra_args=()):
263 263 """Apply options for a collection of TestController objects."""
264 264 for controller in controllers:
265 265 if xunit:
266 266 controller.add_xunit()
267 267 if coverage:
268 268 controller.add_coverage()
269 269 controller.env['IPTEST_SUBPROC_STREAMS'] = subproc_streams
270 270 controller.cmd.extend(extra_args)
271 271
272 272 def do_run(controller):
273 273 try:
274 274 try:
275 275 controller.launch()
276 276 except Exception:
277 277 import traceback
278 278 traceback.print_exc()
279 279 return controller, 1 # signal failure
280 280
281 281 exitcode = controller.wait()
282 282 return controller, exitcode
283 283
284 284 except KeyboardInterrupt:
285 285 return controller, -signal.SIGINT
286 286 finally:
287 287 controller.cleanup()
288 288
289 289 def report():
290 290 """Return a string with a summary report of test-related variables."""
291 291 inf = get_sys_info()
292 292 out = []
293 293 def _add(name, value):
294 294 out.append((name, value))
295 295
296 296 _add('IPython version', inf['ipython_version'])
297 297 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
298 298 _add('IPython package', compress_user(inf['ipython_path']))
299 299 _add('Python version', inf['sys_version'].replace('\n',''))
300 300 _add('sys.executable', compress_user(inf['sys_executable']))
301 301 _add('Platform', inf['platform'])
302 302
303 303 width = max(len(n) for (n,v) in out)
304 304 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
305 305
306 306 avail = []
307 307 not_avail = []
308 308
309 309 for k, is_avail in have.items():
310 310 if is_avail:
311 311 avail.append(k)
312 312 else:
313 313 not_avail.append(k)
314 314
315 315 if avail:
316 316 out.append('\nTools and libraries available at test time:\n')
317 317 avail.sort()
318 318 out.append(' ' + ' '.join(avail)+'\n')
319 319
320 320 if not_avail:
321 321 out.append('\nTools and libraries NOT available at test time:\n')
322 322 not_avail.sort()
323 323 out.append(' ' + ' '.join(not_avail)+'\n')
324 324
325 325 return ''.join(out)
326 326
327 327 def run_iptestall(options):
328 328 """Run the entire IPython test suite by calling nose and trial.
329 329
330 330 This function constructs :class:`IPTester` instances for all IPython
331 331 modules and package and then runs each of them. This causes the modules
332 332 and packages of IPython to be tested each in their own subprocess using
333 333 nose.
334 334
335 335 Parameters
336 336 ----------
337 337
338 338 All parameters are passed as attributes of the options object.
339 339
340 340 testgroups : list of str
341 341 Run only these sections of the test suite. If empty, run all the available
342 342 sections.
343 343
344 344 fast : int or None
345 345 Run the test suite in parallel, using n simultaneous processes. If None
346 346 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
347 347
348 348 inc_slow : bool
349 349 Include slow tests, like IPython.parallel. By default, these tests aren't
350 350 run.
351 351
352 352 xunit : bool
353 353 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
354 354
355 355 coverage : bool or str
356 356 Measure code coverage from tests. True will store the raw coverage data,
357 357 or pass 'html' or 'xml' to get reports.
358 358
359 359 extra_args : list
360 360 Extra arguments to pass to the test subprocesses, e.g. '-v'
361 361 """
362 362 if options.fast != 1:
363 363 # If running in parallel, capture output so it doesn't get interleaved
364 364 TestController.buffer_output = True
365 365
366 366 to_run, not_run = prepare_controllers(options)
367 367
368 368 def justify(ltext, rtext, width=70, fill='-'):
369 369 ltext += ' '
370 370 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
371 371 return ltext + rtext
372 372
373 373 # Run all test runners, tracking execution time
374 374 failed = []
375 375 t_start = time.time()
376 376
377 377 print()
378 378 if options.fast == 1:
379 379 # This actually means sequential, i.e. with 1 job
380 380 for controller in to_run:
381 381 print('IPython test group:', controller.section)
382 382 sys.stdout.flush() # Show in correct order when output is piped
383 383 controller, res = do_run(controller)
384 384 if res:
385 385 failed.append(controller)
386 386 if res == -signal.SIGINT:
387 387 print("Interrupted")
388 388 break
389 389 print()
390 390
391 391 else:
392 392 # Run tests concurrently
393 393 try:
394 394 pool = multiprocessing.pool.ThreadPool(options.fast)
395 395 for (controller, res) in pool.imap_unordered(do_run, to_run):
396 396 res_string = 'OK' if res == 0 else 'FAILED'
397 397 print(justify('IPython test group: ' + controller.section, res_string))
398 398 if res:
399 399 print(bytes_to_str(controller.stdout))
400 400 failed.append(controller)
401 401 if res == -signal.SIGINT:
402 402 print("Interrupted")
403 403 break
404 404 except KeyboardInterrupt:
405 405 return
406 406
407 407 for controller in not_run:
408 408 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
409 409
410 410 t_end = time.time()
411 411 t_tests = t_end - t_start
412 412 nrunners = len(to_run)
413 413 nfail = len(failed)
414 414 # summarize results
415 415 print('_'*70)
416 416 print('Test suite completed for system with the following information:')
417 417 print(report())
418 418 took = "Took %.3fs." % t_tests
419 419 print('Status: ', end='')
420 420 if not failed:
421 421 print('OK (%d test groups).' % nrunners, took)
422 422 else:
423 423 # If anything went wrong, point out what command to rerun manually to
424 424 # see the actual errors and individual summary
425 425 failed_sections = [c.section for c in failed]
426 426 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
427 427 nrunners, ', '.join(failed_sections)), took)
428 428 print()
429 429 print('You may wish to rerun these, with:')
430 430 print(' iptest', *failed_sections)
431 431 print()
432 432
433 433 if options.coverage:
434 434 from coverage import coverage
435 435 cov = coverage(data_file='.coverage')
436 436 cov.combine()
437 437 cov.save()
438 438
439 439 # Coverage HTML report
440 440 if options.coverage == 'html':
441 441 html_dir = 'ipy_htmlcov'
442 442 shutil.rmtree(html_dir, ignore_errors=True)
443 443 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
444 444 sys.stdout.flush()
445 445
446 446 # Custom HTML reporter to clean up module names.
447 447 from coverage.html import HtmlReporter
448 448 class CustomHtmlReporter(HtmlReporter):
449 449 def find_code_units(self, morfs):
450 450 super(CustomHtmlReporter, self).find_code_units(morfs)
451 451 for cu in self.code_units:
452 452 nameparts = cu.name.split(os.sep)
453 453 if 'IPython' not in nameparts:
454 454 continue
455 455 ix = nameparts.index('IPython')
456 456 cu.name = '.'.join(nameparts[ix:])
457 457
458 458 # Reimplement the html_report method with our custom reporter
459 459 cov._harvest_data()
460 460 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
461 461 html_title='IPython test coverage',
462 462 )
463 463 reporter = CustomHtmlReporter(cov, cov.config)
464 464 reporter.report(None)
465 465 print('done.')
466 466
467 467 # Coverage XML report
468 468 elif options.coverage == 'xml':
469 469 cov.xml_report(outfile='ipy_coverage.xml')
470 470
471 471 if failed:
472 472 # Ensure that our exit code indicates failure
473 473 sys.exit(1)
474 474
475 475 argparser = argparse.ArgumentParser(description='Run IPython test suite')
476 476 argparser.add_argument('testgroups', nargs='*',
477 477 help='Run specified groups of tests. If omitted, run '
478 478 'all tests.')
479 479 argparser.add_argument('--all', action='store_true',
480 480 help='Include slow tests not run by default.')
481 481 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
482 482 help='Run test sections in parallel.')
483 483 argparser.add_argument('--xunit', action='store_true',
484 484 help='Produce Xunit XML results')
485 485 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
486 486 help="Measure test coverage. Specify 'html' or "
487 487 "'xml' to get reports.")
488 488 argparser.add_argument('--subproc-streams', default='capture',
489 489 help="What to do with stdout/stderr from subprocesses. "
490 490 "'capture' (default), 'show' and 'discard' are the options.")
491 491
492 492 def default_options():
493 493 """Get an argparse Namespace object with the default arguments, to pass to
494 494 :func:`run_iptestall`.
495 495 """
496 496 options = argparser.parse_args([])
497 497 options.extra_args = []
498 498 return options
499 499
500 500 def main():
501 501 # Arguments after -- should be passed through to nose. Argparse treats
502 502 # everything after -- as regular positional arguments, so we separate them
503 503 # first.
504 504 try:
505 505 ix = sys.argv.index('--')
506 506 except ValueError:
507 507 to_parse = sys.argv[1:]
508 508 extra_args = []
509 509 else:
510 510 to_parse = sys.argv[1:ix]
511 511 extra_args = sys.argv[ix+1:]
512 512
513 513 options = argparser.parse_args(to_parse)
514 514 options.extra_args = extra_args
515 515
516 516 run_iptestall(options)
517 517
518 518
519 519 if __name__ == '__main__':
520 520 main()
General Comments 0
You need to be logged in to leave comments. Login now