##// END OF EJS Templates
don't test js test groups by default...
MinRK -
Show More
@@ -1,592 +1,594 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 json
23 23 import multiprocessing.pool
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, StreamCapturer
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
54 54 def __init__(self):
55 55 self.cmd = []
56 56 self.env = {}
57 57 self.dirs = []
58 58
59 59 def setup(self):
60 60 """Create temporary directories etc.
61 61
62 62 This is only called when we know the test group will be run. Things
63 63 created here may be cleaned up by self.cleanup().
64 64 """
65 65 pass
66 66
67 67 def launch(self, buffer_output=False):
68 68 # print('*** ENV:', self.env) # dbg
69 69 # print('*** CMD:', self.cmd) # dbg
70 70 env = os.environ.copy()
71 71 env.update(self.env)
72 72 output = subprocess.PIPE if buffer_output else None
73 73 stdout = subprocess.STDOUT if buffer_output else None
74 74 self.process = subprocess.Popen(self.cmd, stdout=output,
75 75 stderr=stdout, env=env)
76 76
77 77 def wait(self):
78 78 self.stdout, _ = self.process.communicate()
79 79 return self.process.returncode
80 80
81 81 def print_extra_info(self):
82 82 """Print extra information about this test run.
83 83
84 84 If we're running in parallel and showing the concise view, this is only
85 85 called if the test group fails. Otherwise, it's called before the test
86 86 group is started.
87 87
88 88 The base implementation does nothing, but it can be overridden by
89 89 subclasses.
90 90 """
91 91 return
92 92
93 93 def cleanup_process(self):
94 94 """Cleanup on exit by killing any leftover processes."""
95 95 subp = self.process
96 96 if subp is None or (subp.poll() is not None):
97 97 return # Process doesn't exist, or is already dead.
98 98
99 99 try:
100 100 print('Cleaning up stale PID: %d' % subp.pid)
101 101 subp.kill()
102 102 except: # (OSError, WindowsError) ?
103 103 # This is just a best effort, if we fail or the process was
104 104 # really gone, ignore it.
105 105 pass
106 106 else:
107 107 for i in range(10):
108 108 if subp.poll() is None:
109 109 time.sleep(0.1)
110 110 else:
111 111 break
112 112
113 113 if subp.poll() is None:
114 114 # The process did not die...
115 115 print('... failed. Manual cleanup may be required.')
116 116
117 117 def cleanup(self):
118 118 "Kill process if it's still alive, and clean up temporary directories"
119 119 self.cleanup_process()
120 120 for td in self.dirs:
121 121 td.cleanup()
122 122
123 123 __del__ = cleanup
124 124
125 125 class PyTestController(TestController):
126 126 """Run Python tests using IPython.testing.iptest"""
127 127 #: str, Python command to execute in subprocess
128 128 pycmd = None
129 129
130 130 def __init__(self, section, options):
131 131 """Create new test runner."""
132 132 TestController.__init__(self)
133 133 self.section = section
134 134 # pycmd is put into cmd[2] in PyTestController.launch()
135 135 self.cmd = [sys.executable, '-c', None, section]
136 136 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
137 137 self.options = options
138 138
139 139 def setup(self):
140 140 ipydir = TemporaryDirectory()
141 141 self.dirs.append(ipydir)
142 142 self.env['IPYTHONDIR'] = ipydir.name
143 143 self.workingdir = workingdir = TemporaryDirectory()
144 144 self.dirs.append(workingdir)
145 145 self.env['IPTEST_WORKING_DIR'] = workingdir.name
146 146 # This means we won't get odd effects from our own matplotlib config
147 147 self.env['MPLCONFIGDIR'] = workingdir.name
148 148
149 149 # From options:
150 150 if self.options.xunit:
151 151 self.add_xunit()
152 152 if self.options.coverage:
153 153 self.add_coverage()
154 154 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
155 155 self.cmd.extend(self.options.extra_args)
156 156
157 157 @property
158 158 def will_run(self):
159 159 try:
160 160 return test_sections[self.section].will_run
161 161 except KeyError:
162 162 return True
163 163
164 164 def add_xunit(self):
165 165 xunit_file = os.path.abspath(self.section + '.xunit.xml')
166 166 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
167 167
168 168 def add_coverage(self):
169 169 try:
170 170 sources = test_sections[self.section].includes
171 171 except KeyError:
172 172 sources = ['IPython']
173 173
174 174 coverage_rc = ("[run]\n"
175 175 "data_file = {data_file}\n"
176 176 "source =\n"
177 177 " {source}\n"
178 178 ).format(data_file=os.path.abspath('.coverage.'+self.section),
179 179 source="\n ".join(sources))
180 180 config_file = os.path.join(self.workingdir.name, '.coveragerc')
181 181 with open(config_file, 'w') as f:
182 182 f.write(coverage_rc)
183 183
184 184 self.env['COVERAGE_PROCESS_START'] = config_file
185 185 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
186 186
187 187 def launch(self, buffer_output=False):
188 188 self.cmd[2] = self.pycmd
189 189 super(PyTestController, self).launch(buffer_output=buffer_output)
190 190
191 191 js_prefix = 'js/'
192 192
193 193 def get_js_test_dir():
194 194 import IPython.html.tests as t
195 195 return os.path.join(os.path.dirname(t.__file__), '')
196 196
197 197 def all_js_groups():
198 198 import glob
199 199 test_dir = get_js_test_dir()
200 200 all_subdirs = glob.glob(test_dir + '*/')
201 201 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
202 202
203 203 class JSController(TestController):
204 204 """Run CasperJS tests """
205 205 def __init__(self, section):
206 206 """Create new test runner."""
207 207 TestController.__init__(self)
208 208 self.section = section
209 209 js_test_dir = get_js_test_dir()
210 210 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
211 211 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
212 212 self.cmd = ['casperjs', 'test', includes, test_cases]
213 213
214 214 def setup(self):
215 215 self.ipydir = TemporaryDirectory()
216 216 self.nbdir = TemporaryDirectory()
217 217 self.dirs.append(self.ipydir)
218 218 self.dirs.append(self.nbdir)
219 219 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
220 220 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
221 221
222 222 # start the ipython notebook, so we get the port number
223 223 self.server_port = 0
224 224 self._init_server()
225 225 if self.server_port:
226 226 self.cmd.append("--port=%i" % self.server_port)
227 227 else:
228 228 # don't launch tests if the server didn't start
229 229 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
230 230
231 231 def print_extra_info(self):
232 232 print("Running tests with notebook directory %r" % self.nbdir.name)
233 233
234 234 @property
235 235 def will_run(self):
236 236 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3'])
237 237
238 238 def _init_server(self):
239 239 "Start the notebook server in a separate process"
240 240 self.server_command = command = [sys.executable,
241 241 '-m', 'IPython.html',
242 242 '--no-browser',
243 243 '--ipython-dir', self.ipydir.name,
244 244 '--notebook-dir', self.nbdir.name,
245 245 ]
246 246 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
247 247 # which run afoul of ipc's maximum path length.
248 248 if sys.platform.startswith('linux'):
249 249 command.append('--KernelManager.transport=ipc')
250 250 self.stream_capturer = c = StreamCapturer()
251 251 c.start()
252 252 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT)
253 253 self.server_info_file = os.path.join(self.ipydir.name,
254 254 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
255 255 )
256 256 self._wait_for_server()
257 257
258 258 def _wait_for_server(self):
259 259 """Wait 30 seconds for the notebook server to start"""
260 260 for i in range(300):
261 261 if self.server.poll() is not None:
262 262 return self._failed_to_start()
263 263 if os.path.exists(self.server_info_file):
264 264 self._load_server_info()
265 265 return
266 266 time.sleep(0.1)
267 267 print("Notebook server-info file never arrived: %s" % self.server_info_file,
268 268 file=sys.stderr
269 269 )
270 270
271 271 def _failed_to_start(self):
272 272 """Notebook server exited prematurely"""
273 273 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
274 274 print("Notebook failed to start: ", file=sys.stderr)
275 275 print(self.server_command)
276 276 print(captured, file=sys.stderr)
277 277
278 278 def _load_server_info(self):
279 279 """Notebook server started, load connection info from JSON"""
280 280 with open(self.server_info_file) as f:
281 281 info = json.load(f)
282 282 self.server_port = info['port']
283 283
284 284 def cleanup(self):
285 285 try:
286 286 self.server.terminate()
287 287 except OSError:
288 288 # already dead
289 289 pass
290 290 self.server.wait()
291 291 self.stream_capturer.halt()
292 292 TestController.cleanup(self)
293 293
294 294
295 295 def prepare_controllers(options):
296 296 """Returns two lists of TestController instances, those to run, and those
297 297 not to run."""
298 298 testgroups = options.testgroups
299 299
300 300 if testgroups:
301 301 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
302 302 or g.startswith('IPython.')]
303 303 if 'js' in testgroups:
304 304 js_testgroups = all_js_groups()
305 305 else:
306 306 js_testgroups = [g for g in testgroups if g not in py_testgroups]
307 307 else:
308 308 py_testgroups = py_test_group_names
309 js_testgroups = all_js_groups()
310 309 if not options.all:
310 js_testgroups = []
311 311 test_sections['parallel'].enabled = False
312 else:
313 js_testgroups = all_js_groups()
312 314
313 315 c_js = [JSController(name) for name in js_testgroups]
314 316 c_py = [PyTestController(name, options) for name in py_testgroups]
315 317
316 318 controllers = c_py + c_js
317 319 to_run = [c for c in controllers if c.will_run]
318 320 not_run = [c for c in controllers if not c.will_run]
319 321 return to_run, not_run
320 322
321 323 def do_run(controller, buffer_output=True):
322 324 """Setup and run a test controller.
323 325
324 326 If buffer_output is True, no output is displayed, to avoid it appearing
325 327 interleaved. In this case, the caller is responsible for displaying test
326 328 output on failure.
327 329
328 330 Returns
329 331 -------
330 332 controller : TestController
331 333 The same controller as passed in, as a convenience for using map() type
332 334 APIs.
333 335 exitcode : int
334 336 The exit code of the test subprocess. Non-zero indicates failure.
335 337 """
336 338 try:
337 339 try:
338 340 controller.setup()
339 341 if not buffer_output:
340 342 controller.print_extra_info()
341 343 controller.launch(buffer_output=buffer_output)
342 344 except Exception:
343 345 import traceback
344 346 traceback.print_exc()
345 347 return controller, 1 # signal failure
346 348
347 349 exitcode = controller.wait()
348 350 return controller, exitcode
349 351
350 352 except KeyboardInterrupt:
351 353 return controller, -signal.SIGINT
352 354 finally:
353 355 controller.cleanup()
354 356
355 357 def report():
356 358 """Return a string with a summary report of test-related variables."""
357 359 inf = get_sys_info()
358 360 out = []
359 361 def _add(name, value):
360 362 out.append((name, value))
361 363
362 364 _add('IPython version', inf['ipython_version'])
363 365 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
364 366 _add('IPython package', compress_user(inf['ipython_path']))
365 367 _add('Python version', inf['sys_version'].replace('\n',''))
366 368 _add('sys.executable', compress_user(inf['sys_executable']))
367 369 _add('Platform', inf['platform'])
368 370
369 371 width = max(len(n) for (n,v) in out)
370 372 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
371 373
372 374 avail = []
373 375 not_avail = []
374 376
375 377 for k, is_avail in have.items():
376 378 if is_avail:
377 379 avail.append(k)
378 380 else:
379 381 not_avail.append(k)
380 382
381 383 if avail:
382 384 out.append('\nTools and libraries available at test time:\n')
383 385 avail.sort()
384 386 out.append(' ' + ' '.join(avail)+'\n')
385 387
386 388 if not_avail:
387 389 out.append('\nTools and libraries NOT available at test time:\n')
388 390 not_avail.sort()
389 391 out.append(' ' + ' '.join(not_avail)+'\n')
390 392
391 393 return ''.join(out)
392 394
393 395 def run_iptestall(options):
394 396 """Run the entire IPython test suite by calling nose and trial.
395 397
396 398 This function constructs :class:`IPTester` instances for all IPython
397 399 modules and package and then runs each of them. This causes the modules
398 400 and packages of IPython to be tested each in their own subprocess using
399 401 nose.
400 402
401 403 Parameters
402 404 ----------
403 405
404 406 All parameters are passed as attributes of the options object.
405 407
406 408 testgroups : list of str
407 409 Run only these sections of the test suite. If empty, run all the available
408 410 sections.
409 411
410 412 fast : int or None
411 413 Run the test suite in parallel, using n simultaneous processes. If None
412 414 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
413 415
414 416 inc_slow : bool
415 417 Include slow tests, like IPython.parallel. By default, these tests aren't
416 418 run.
417 419
418 420 xunit : bool
419 421 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
420 422
421 423 coverage : bool or str
422 424 Measure code coverage from tests. True will store the raw coverage data,
423 425 or pass 'html' or 'xml' to get reports.
424 426
425 427 extra_args : list
426 428 Extra arguments to pass to the test subprocesses, e.g. '-v'
427 429 """
428 430 to_run, not_run = prepare_controllers(options)
429 431
430 432 def justify(ltext, rtext, width=70, fill='-'):
431 433 ltext += ' '
432 434 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
433 435 return ltext + rtext
434 436
435 437 # Run all test runners, tracking execution time
436 438 failed = []
437 439 t_start = time.time()
438 440
439 441 print()
440 442 if options.fast == 1:
441 443 # This actually means sequential, i.e. with 1 job
442 444 for controller in to_run:
443 445 print('Test group:', controller.section)
444 446 sys.stdout.flush() # Show in correct order when output is piped
445 447 controller, res = do_run(controller, buffer_output=False)
446 448 if res:
447 449 failed.append(controller)
448 450 if res == -signal.SIGINT:
449 451 print("Interrupted")
450 452 break
451 453 print()
452 454
453 455 else:
454 456 # Run tests concurrently
455 457 try:
456 458 pool = multiprocessing.pool.ThreadPool(options.fast)
457 459 for (controller, res) in pool.imap_unordered(do_run, to_run):
458 460 res_string = 'OK' if res == 0 else 'FAILED'
459 461 print(justify('Test group: ' + controller.section, res_string))
460 462 if res:
461 463 controller.print_extra_info()
462 464 print(bytes_to_str(controller.stdout))
463 465 failed.append(controller)
464 466 if res == -signal.SIGINT:
465 467 print("Interrupted")
466 468 break
467 469 except KeyboardInterrupt:
468 470 return
469 471
470 472 for controller in not_run:
471 473 print(justify('Test group: ' + controller.section, 'NOT RUN'))
472 474
473 475 t_end = time.time()
474 476 t_tests = t_end - t_start
475 477 nrunners = len(to_run)
476 478 nfail = len(failed)
477 479 # summarize results
478 480 print('_'*70)
479 481 print('Test suite completed for system with the following information:')
480 482 print(report())
481 483 took = "Took %.3fs." % t_tests
482 484 print('Status: ', end='')
483 485 if not failed:
484 486 print('OK (%d test groups).' % nrunners, took)
485 487 else:
486 488 # If anything went wrong, point out what command to rerun manually to
487 489 # see the actual errors and individual summary
488 490 failed_sections = [c.section for c in failed]
489 491 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
490 492 nrunners, ', '.join(failed_sections)), took)
491 493 print()
492 494 print('You may wish to rerun these, with:')
493 495 print(' iptest', *failed_sections)
494 496 print()
495 497
496 498 if options.coverage:
497 499 from coverage import coverage
498 500 cov = coverage(data_file='.coverage')
499 501 cov.combine()
500 502 cov.save()
501 503
502 504 # Coverage HTML report
503 505 if options.coverage == 'html':
504 506 html_dir = 'ipy_htmlcov'
505 507 shutil.rmtree(html_dir, ignore_errors=True)
506 508 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
507 509 sys.stdout.flush()
508 510
509 511 # Custom HTML reporter to clean up module names.
510 512 from coverage.html import HtmlReporter
511 513 class CustomHtmlReporter(HtmlReporter):
512 514 def find_code_units(self, morfs):
513 515 super(CustomHtmlReporter, self).find_code_units(morfs)
514 516 for cu in self.code_units:
515 517 nameparts = cu.name.split(os.sep)
516 518 if 'IPython' not in nameparts:
517 519 continue
518 520 ix = nameparts.index('IPython')
519 521 cu.name = '.'.join(nameparts[ix:])
520 522
521 523 # Reimplement the html_report method with our custom reporter
522 524 cov._harvest_data()
523 525 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
524 526 html_title='IPython test coverage',
525 527 )
526 528 reporter = CustomHtmlReporter(cov, cov.config)
527 529 reporter.report(None)
528 530 print('done.')
529 531
530 532 # Coverage XML report
531 533 elif options.coverage == 'xml':
532 534 cov.xml_report(outfile='ipy_coverage.xml')
533 535
534 536 if failed:
535 537 # Ensure that our exit code indicates failure
536 538 sys.exit(1)
537 539
538 540 argparser = argparse.ArgumentParser(description='Run IPython test suite')
539 541 argparser.add_argument('testgroups', nargs='*',
540 542 help='Run specified groups of tests. If omitted, run '
541 543 'all tests.')
542 544 argparser.add_argument('--all', action='store_true',
543 545 help='Include slow tests not run by default.')
544 546 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
545 547 help='Run test sections in parallel. This starts as many '
546 548 'processes as you have cores, or you can specify a number.')
547 549 argparser.add_argument('--xunit', action='store_true',
548 550 help='Produce Xunit XML results')
549 551 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
550 552 help="Measure test coverage. Specify 'html' or "
551 553 "'xml' to get reports.")
552 554 argparser.add_argument('--subproc-streams', default='capture',
553 555 help="What to do with stdout/stderr from subprocesses. "
554 556 "'capture' (default), 'show' and 'discard' are the options.")
555 557
556 558 def default_options():
557 559 """Get an argparse Namespace object with the default arguments, to pass to
558 560 :func:`run_iptestall`.
559 561 """
560 562 options = argparser.parse_args([])
561 563 options.extra_args = []
562 564 return options
563 565
564 566 def main():
565 567 # iptest doesn't work correctly if the working directory is the
566 568 # root of the IPython source tree. Tell the user to avoid
567 569 # frustration.
568 570 if os.path.exists(os.path.join(os.getcwd(),
569 571 'IPython', 'testing', '__main__.py')):
570 572 print("Don't run iptest from the IPython source directory",
571 573 file=sys.stderr)
572 574 sys.exit(1)
573 575 # Arguments after -- should be passed through to nose. Argparse treats
574 576 # everything after -- as regular positional arguments, so we separate them
575 577 # first.
576 578 try:
577 579 ix = sys.argv.index('--')
578 580 except ValueError:
579 581 to_parse = sys.argv[1:]
580 582 extra_args = []
581 583 else:
582 584 to_parse = sys.argv[1:ix]
583 585 extra_args = sys.argv[ix+1:]
584 586
585 587 options = argparser.parse_args(to_parse)
586 588 options.extra_args = extra_args
587 589
588 590 run_iptestall(options)
589 591
590 592
591 593 if __name__ == '__main__':
592 594 main()
General Comments 0
You need to be logged in to leave comments. Login now