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