##// END OF EJS Templates
Revert "Enabled IPython Server output for Travis debugging"...
Jonathan Frederic -
Show More
@@ -1,510 +1,507
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 class JSController(TestController):
163 163 """Run CasperJS tests """
164 164 def __init__(self, section):
165 165 """Create new test runner."""
166 166 TestController.__init__(self)
167 167 self.section = section
168 168
169 169 self.ipydir = TemporaryDirectory()
170 170 self.dirs.append(self.ipydir)
171 171 self.env['IPYTHONDIR'] = self.ipydir.name
172 172
173 173 def launch(self):
174 174 # start the ipython notebook, so we get the port number
175 175 self._init_server()
176 176
177 177 import IPython.html.tests as t
178 178 test_dir = os.path.join(os.path.dirname(t.__file__), 'casperjs')
179 179 includes = '--includes=' + os.path.join(test_dir,'util.js')
180 180 test_cases = os.path.join(test_dir, 'test_cases')
181 181 port = '--port=' + str(self.server_port)
182 182 self.cmd = ['casperjs', 'test', port, includes, test_cases]
183 183
184 184 super(JSController, self).launch()
185 185
186 186 @property
187 187 def will_run(self):
188 188 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs'])
189 189
190 190 def _init_server(self):
191 191 "Start the notebook server in a separate process"
192 192 self.queue = q = Queue()
193 193 self.server = Process(target=run_webapp, args=(q, self.ipydir.name))
194 194 self.server.start()
195 195 self.server_port = q.get()
196 196
197 197 def cleanup(self):
198 198 self.server.terminate()
199 199 self.server.join()
200 200 TestController.cleanup(self)
201 201
202 202 js_test_group_names = {'js'}
203 203
204 204 def run_webapp(q, nbdir, loglevel=0):
205 205 """start the IPython Notebook, and pass port back to the queue"""
206 206 import os
207 207 import IPython.html.notebookapp as nbapp
208 208 import sys
209 #sys.stdout = open(str(os.getpid()) + ".out", "w")
210 #sys.stderr = open(str(os.getpid()) + ".err", "w")
211 print("hello")
212 # sys.stderr = open(os.devnull, 'w')
209 sys.stderr = open(os.devnull, 'w')
213 210 os.environ["IPYTHONDIR"] = nbdir
214 211 server = nbapp.NotebookApp()
215 212 args = ['--no-browser']
216 213 args.append('--notebook-dir='+nbdir)
217 214 args.append('--profile-dir='+nbdir)
218 #args.append('--log-level='+str(loglevel))
215 args.append('--log-level='+str(loglevel))
219 216 server.initialize(args)
220 217 # communicate the port number to the parent process
221 218 q.put(server.port)
222 219 server.start()
223 220
224 221 def prepare_controllers(options):
225 222 """Returns two lists of TestController instances, those to run, and those
226 223 not to run."""
227 224 testgroups = options.testgroups
228 225
229 226 if testgroups:
230 227 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
231 228 or g.startswith('IPython')]
232 229 js_testgroups = [g for g in testgroups if g in js_test_group_names]
233 230 else:
234 231 py_testgroups = py_test_group_names
235 232 js_testgroups = js_test_group_names
236 233 if not options.all:
237 234 test_sections['parallel'].enabled = False
238 235
239 236 c_js = [JSController(name) for name in js_testgroups]
240 237 c_py = [PyTestController(name) for name in py_testgroups]
241 238
242 239 configure_py_controllers(c_py, xunit=options.xunit,
243 240 coverage=options.coverage, subproc_streams=options.subproc_streams,
244 241 extra_args=options.extra_args)
245 242
246 243 controllers = c_py + c_js
247 244 to_run = [c for c in controllers if c.will_run]
248 245 not_run = [c for c in controllers if not c.will_run]
249 246 return to_run, not_run
250 247
251 248 def configure_py_controllers(controllers, xunit=False, coverage=False,
252 249 subproc_streams='capture', extra_args=()):
253 250 """Apply options for a collection of TestController objects."""
254 251 for controller in controllers:
255 252 if xunit:
256 253 controller.add_xunit()
257 254 if coverage:
258 255 controller.add_coverage()
259 256 controller.env['IPTEST_SUBPROC_STREAMS'] = subproc_streams
260 257 controller.cmd.extend(extra_args)
261 258
262 259 def do_run(controller):
263 260 try:
264 261 try:
265 262 controller.launch()
266 263 except Exception:
267 264 import traceback
268 265 traceback.print_exc()
269 266 return controller, 1 # signal failure
270 267
271 268 exitcode = controller.wait()
272 269 return controller, exitcode
273 270
274 271 except KeyboardInterrupt:
275 272 return controller, -signal.SIGINT
276 273 finally:
277 274 controller.cleanup()
278 275
279 276 def report():
280 277 """Return a string with a summary report of test-related variables."""
281 278 inf = get_sys_info()
282 279 out = []
283 280 def _add(name, value):
284 281 out.append((name, value))
285 282
286 283 _add('IPython version', inf['ipython_version'])
287 284 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
288 285 _add('IPython package', compress_user(inf['ipython_path']))
289 286 _add('Python version', inf['sys_version'].replace('\n',''))
290 287 _add('sys.executable', compress_user(inf['sys_executable']))
291 288 _add('Platform', inf['platform'])
292 289
293 290 width = max(len(n) for (n,v) in out)
294 291 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
295 292
296 293 avail = []
297 294 not_avail = []
298 295
299 296 for k, is_avail in have.items():
300 297 if is_avail:
301 298 avail.append(k)
302 299 else:
303 300 not_avail.append(k)
304 301
305 302 if avail:
306 303 out.append('\nTools and libraries available at test time:\n')
307 304 avail.sort()
308 305 out.append(' ' + ' '.join(avail)+'\n')
309 306
310 307 if not_avail:
311 308 out.append('\nTools and libraries NOT available at test time:\n')
312 309 not_avail.sort()
313 310 out.append(' ' + ' '.join(not_avail)+'\n')
314 311
315 312 return ''.join(out)
316 313
317 314 def run_iptestall(options):
318 315 """Run the entire IPython test suite by calling nose and trial.
319 316
320 317 This function constructs :class:`IPTester` instances for all IPython
321 318 modules and package and then runs each of them. This causes the modules
322 319 and packages of IPython to be tested each in their own subprocess using
323 320 nose.
324 321
325 322 Parameters
326 323 ----------
327 324
328 325 All parameters are passed as attributes of the options object.
329 326
330 327 testgroups : list of str
331 328 Run only these sections of the test suite. If empty, run all the available
332 329 sections.
333 330
334 331 fast : int or None
335 332 Run the test suite in parallel, using n simultaneous processes. If None
336 333 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
337 334
338 335 inc_slow : bool
339 336 Include slow tests, like IPython.parallel. By default, these tests aren't
340 337 run.
341 338
342 339 xunit : bool
343 340 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
344 341
345 342 coverage : bool or str
346 343 Measure code coverage from tests. True will store the raw coverage data,
347 344 or pass 'html' or 'xml' to get reports.
348 345
349 346 extra_args : list
350 347 Extra arguments to pass to the test subprocesses, e.g. '-v'
351 348 """
352 349 if options.fast != 1:
353 350 # If running in parallel, capture output so it doesn't get interleaved
354 351 TestController.buffer_output = True
355 352
356 353 to_run, not_run = prepare_controllers(options)
357 354
358 355 def justify(ltext, rtext, width=70, fill='-'):
359 356 ltext += ' '
360 357 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
361 358 return ltext + rtext
362 359
363 360 # Run all test runners, tracking execution time
364 361 failed = []
365 362 t_start = time.time()
366 363
367 364 print()
368 365 if options.fast == 1:
369 366 # This actually means sequential, i.e. with 1 job
370 367 for controller in to_run:
371 368 print('IPython test group:', controller.section)
372 369 sys.stdout.flush() # Show in correct order when output is piped
373 370 controller, res = do_run(controller)
374 371 if res:
375 372 failed.append(controller)
376 373 if res == -signal.SIGINT:
377 374 print("Interrupted")
378 375 break
379 376 print()
380 377
381 378 else:
382 379 # Run tests concurrently
383 380 try:
384 381 pool = multiprocessing.pool.ThreadPool(options.fast)
385 382 for (controller, res) in pool.imap_unordered(do_run, to_run):
386 383 res_string = 'OK' if res == 0 else 'FAILED'
387 384 print(justify('IPython test group: ' + controller.section, res_string))
388 385 if res:
389 386 print(bytes_to_str(controller.stdout))
390 387 failed.append(controller)
391 388 if res == -signal.SIGINT:
392 389 print("Interrupted")
393 390 break
394 391 except KeyboardInterrupt:
395 392 return
396 393
397 394 for controller in not_run:
398 395 print(justify('IPython test group: ' + controller.section, 'NOT RUN'))
399 396
400 397 t_end = time.time()
401 398 t_tests = t_end - t_start
402 399 nrunners = len(to_run)
403 400 nfail = len(failed)
404 401 # summarize results
405 402 print('_'*70)
406 403 print('Test suite completed for system with the following information:')
407 404 print(report())
408 405 took = "Took %.3fs." % t_tests
409 406 print('Status: ', end='')
410 407 if not failed:
411 408 print('OK (%d test groups).' % nrunners, took)
412 409 else:
413 410 # If anything went wrong, point out what command to rerun manually to
414 411 # see the actual errors and individual summary
415 412 failed_sections = [c.section for c in failed]
416 413 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
417 414 nrunners, ', '.join(failed_sections)), took)
418 415 print()
419 416 print('You may wish to rerun these, with:')
420 417 print(' iptest', *failed_sections)
421 418 print()
422 419
423 420 if options.coverage:
424 421 from coverage import coverage
425 422 cov = coverage(data_file='.coverage')
426 423 cov.combine()
427 424 cov.save()
428 425
429 426 # Coverage HTML report
430 427 if options.coverage == 'html':
431 428 html_dir = 'ipy_htmlcov'
432 429 shutil.rmtree(html_dir, ignore_errors=True)
433 430 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
434 431 sys.stdout.flush()
435 432
436 433 # Custom HTML reporter to clean up module names.
437 434 from coverage.html import HtmlReporter
438 435 class CustomHtmlReporter(HtmlReporter):
439 436 def find_code_units(self, morfs):
440 437 super(CustomHtmlReporter, self).find_code_units(morfs)
441 438 for cu in self.code_units:
442 439 nameparts = cu.name.split(os.sep)
443 440 if 'IPython' not in nameparts:
444 441 continue
445 442 ix = nameparts.index('IPython')
446 443 cu.name = '.'.join(nameparts[ix:])
447 444
448 445 # Reimplement the html_report method with our custom reporter
449 446 cov._harvest_data()
450 447 cov.config.from_args(omit='*%stests' % os.sep, html_dir=html_dir,
451 448 html_title='IPython test coverage',
452 449 )
453 450 reporter = CustomHtmlReporter(cov, cov.config)
454 451 reporter.report(None)
455 452 print('done.')
456 453
457 454 # Coverage XML report
458 455 elif options.coverage == 'xml':
459 456 cov.xml_report(outfile='ipy_coverage.xml')
460 457
461 458 if failed:
462 459 # Ensure that our exit code indicates failure
463 460 sys.exit(1)
464 461
465 462 argparser = argparse.ArgumentParser(description='Run IPython test suite')
466 463 argparser.add_argument('testgroups', nargs='*',
467 464 help='Run specified groups of tests. If omitted, run '
468 465 'all tests.')
469 466 argparser.add_argument('--all', action='store_true',
470 467 help='Include slow tests not run by default.')
471 468 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
472 469 help='Run test sections in parallel.')
473 470 argparser.add_argument('--xunit', action='store_true',
474 471 help='Produce Xunit XML results')
475 472 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
476 473 help="Measure test coverage. Specify 'html' or "
477 474 "'xml' to get reports.")
478 475 argparser.add_argument('--subproc-streams', default='capture',
479 476 help="What to do with stdout/stderr from subprocesses. "
480 477 "'capture' (default), 'show' and 'discard' are the options.")
481 478
482 479 def default_options():
483 480 """Get an argparse Namespace object with the default arguments, to pass to
484 481 :func:`run_iptestall`.
485 482 """
486 483 options = argparser.parse_args([])
487 484 options.extra_args = []
488 485 return options
489 486
490 487 def main():
491 488 # Arguments after -- should be passed through to nose. Argparse treats
492 489 # everything after -- as regular positional arguments, so we separate them
493 490 # first.
494 491 try:
495 492 ix = sys.argv.index('--')
496 493 except ValueError:
497 494 to_parse = sys.argv[1:]
498 495 extra_args = []
499 496 else:
500 497 to_parse = sys.argv[1:ix]
501 498 extra_args = sys.argv[ix+1:]
502 499
503 500 options = argparser.parse_args(to_parse)
504 501 options.extra_args = extra_args
505 502
506 503 run_iptestall(options)
507 504
508 505
509 506 if __name__ == '__main__':
510 507 main()
General Comments 0
You need to be logged in to leave comments. Login now