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