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