OLD | NEW |
1 # Copyright (c) 2014 The Native Client Authors. All rights reserved. | 1 # Copyright (c) 2014 The Native Client Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | |
5 """Test harness for testing chrome apps / extensions.""" | 4 """Test harness for testing chrome apps / extensions.""" |
6 | 5 |
7 import argparse | 6 import argparse |
8 import cStringIO | 7 import cStringIO |
9 import contextlib | 8 import contextlib |
10 import hashlib | 9 import hashlib |
11 import logging | 10 import logging |
12 import os | 11 import os |
13 import shutil | 12 import shutil |
14 import subprocess | 13 import subprocess |
15 import sys | 14 import sys |
16 import tempfile | 15 import tempfile |
17 import threading | 16 import threading |
18 import urllib | 17 import urllib |
19 import urllib2 | 18 import urllib2 |
20 import urlparse | 19 import urlparse |
21 import zipfile | 20 import zipfile |
22 | 21 |
23 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) | 22 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) |
24 SRC_DIR = os.path.dirname(SCRIPT_DIR) | 23 SRC_DIR = os.path.dirname(SCRIPT_DIR) |
25 sys.path.insert(0, os.path.join(SRC_DIR, 'build_tools')) | 24 sys.path.insert(0, os.path.join(SRC_DIR, 'build_tools')) |
26 sys.path.insert(0, os.path.join(SRC_DIR, 'lib')) | 25 sys.path.insert(0, os.path.join(SRC_DIR, 'lib')) |
27 | 26 |
28 import httpd | 27 import httpd |
29 import naclports | 28 import naclports |
30 | 29 |
31 | |
32 # Pinned chrome revision. Update this to pull in a new chrome. | 30 # Pinned chrome revision. Update this to pull in a new chrome. |
33 # Try to select a version that exists on all platforms. | 31 # Try to select a version that exists on all platforms. |
34 CHROME_REVISION = '311001' | 32 CHROME_REVISION = '311001' |
35 | 33 |
36 CHROME_SYNC_DIR = os.path.join(naclports.paths.OUT_DIR, 'downloaded_chrome') | 34 CHROME_SYNC_DIR = os.path.join(naclports.paths.OUT_DIR, 'downloaded_chrome') |
37 | 35 |
38 GS_URL = 'http://storage.googleapis.com' | 36 GS_URL = 'http://storage.googleapis.com' |
39 CHROME_URL_FORMAT = GS_URL + '/chromium-browser-continuous/%s/%s/%s' | 37 CHROME_URL_FORMAT = GS_URL + '/chromium-browser-continuous/%s/%s/%s' |
40 | 38 |
41 TESTING_LIB = os.path.join(SCRIPT_DIR, 'chrome_test.js') | 39 TESTING_LIB = os.path.join(SCRIPT_DIR, 'chrome_test.js') |
42 TESTING_EXTENSION = os.path.join(SCRIPT_DIR, 'extension') | 40 TESTING_EXTENSION = os.path.join(SCRIPT_DIR, 'extension') |
43 TESTING_TCP_APP = os.path.join(SCRIPT_DIR, 'tcpapp'); | 41 TESTING_TCP_APP = os.path.join(SCRIPT_DIR, 'tcpapp') |
44 | 42 |
45 RETURNCODE_KILL = -9 | 43 RETURNCODE_KILL = -9 |
46 | 44 |
47 LOG_LEVEL_MAP = { | 45 LOG_LEVEL_MAP = { |
48 'ERROR': logging.ERROR, | 46 'ERROR': logging.ERROR, |
49 'WARNING': logging.WARNING, | 47 'WARNING': logging.WARNING, |
50 'INFO': logging.INFO, | 48 'INFO': logging.INFO, |
51 'DEBUG': logging.DEBUG, | 49 'DEBUG': logging.DEBUG, |
52 } | 50 } |
53 | 51 |
(...skipping 156 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
210 the taskkill.exe utility for now. | 208 the taskkill.exe utility for now. |
211 | 209 |
212 Args: | 210 Args: |
213 proc: A subprocess.Popen process. | 211 proc: A subprocess.Popen process. |
214 """ | 212 """ |
215 if sys.platform == 'win32': | 213 if sys.platform == 'win32': |
216 # Do subprocess call as the process may terminate before we manage | 214 # Do subprocess call as the process may terminate before we manage |
217 # to invoke taskkill. | 215 # to invoke taskkill. |
218 subprocess.call( | 216 subprocess.call( |
219 [os.path.join(os.environ['SYSTEMROOT'], 'System32', 'taskkill.exe'), | 217 [os.path.join(os.environ['SYSTEMROOT'], 'System32', 'taskkill.exe'), |
220 '/F', '/T', '/PID', str(proc.pid)]) | 218 '/F', '/T', '/PID', str(proc.pid)]) |
221 else: | 219 else: |
222 # Send SIGKILL=9 to the entire process group associated with the child. | 220 # Send SIGKILL=9 to the entire process group associated with the child. |
223 os.kill(-proc.pid, 9) | 221 os.kill(-proc.pid, 9) |
224 | 222 |
225 | 223 |
226 def CommunicateWithTimeout(proc, timeout): | 224 def CommunicateWithTimeout(proc, timeout): |
227 """Wait for a subprocess.Popen to end, capturing output, with a timeout. | 225 """Wait for a subprocess.Popen to end, capturing output, with a timeout. |
228 | 226 |
229 Args: | 227 Args: |
230 proc: A subprocess.Popen. | 228 proc: A subprocess.Popen. |
231 timeout: A timeout in seconds. | 229 timeout: A timeout in seconds. |
232 Returns: | 230 Returns: |
233 (stdout, stderr, returncode). | 231 (stdout, stderr, returncode). |
234 """ | 232 """ |
235 if timeout == 0: | 233 if timeout == 0: |
236 timeout = None | 234 timeout = None |
237 | 235 |
238 result = [] | 236 result = [] |
| 237 |
239 def Target(): | 238 def Target(): |
240 result.append(list(proc.communicate())) | 239 result.append(list(proc.communicate())) |
241 | 240 |
242 thread = threading.Thread(target=Target) | 241 thread = threading.Thread(target=Target) |
243 thread.start() | 242 thread.start() |
244 try: | 243 try: |
245 thread.join(timeout) | 244 thread.join(timeout) |
246 if thread.is_alive(): | 245 if thread.is_alive(): |
247 logging.error('Attempting to kill test due to timeout of %.1f seconds!' % | 246 logging.error('Attempting to kill test due to timeout of %.1f seconds!' % |
248 timeout) | 247 timeout) |
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
333 # Allow the tests to send out log messages. | 332 # Allow the tests to send out log messages. |
334 if ('log' in params and len(params['log']) == 1 and | 333 if ('log' in params and len(params['log']) == 1 and |
335 'level' in params and len(params['level']) == 1): | 334 'level' in params and len(params['level']) == 1): |
336 level = LOG_LEVEL_MAP.get(params['level'][0], logging.ERROR) | 335 level = LOG_LEVEL_MAP.get(params['level'][0], logging.ERROR) |
337 message = params['log'][0] | 336 message = params['log'][0] |
338 logging.log(level, message) | 337 logging.log(level, message) |
339 self.SendEmptyReply() | 338 self.SendEmptyReply() |
340 return | 339 return |
341 # Allow the tests to request the current test filter string. | 340 # Allow the tests to request the current test filter string. |
342 elif ('filter' in params and len(params['filter']) == 1 and | 341 elif ('filter' in params and len(params['filter']) == 1 and |
343 params['filter'][0] == '1'): | 342 params['filter'][0] == '1'): |
344 self.send_response(200, 'OK') | 343 self.send_response(200, 'OK') |
345 self.send_header('Content-type', 'text/html') | 344 self.send_header('Content-type', 'text/html') |
346 self.send_header('Content-length', str(len(self.server.filter_string))) | 345 self.send_header('Content-length', str(len(self.server.filter_string))) |
347 self.end_headers() | 346 self.end_headers() |
348 self.wfile.write(self.server.filter_string) | 347 self.wfile.write(self.server.filter_string) |
349 return | 348 return |
350 # Allow the tests to declare their name on start. | 349 # Allow the tests to declare their name on start. |
351 elif ('start' in params and len(params['start']) == 1 and | 350 elif ('start' in params and len(params['start']) == 1 and |
352 params['start'][0] == '1' and | 351 params['start'][0] == '1' and |
353 'name' in params and len(params['name']) == 1): | 352 'name' in params and len(params['name']) == 1): |
(...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
445 enable_nacl_debug: Boolean indicating that NaCl debugging should be | 444 enable_nacl_debug: Boolean indicating that NaCl debugging should be |
446 enabled. | 445 enabled. |
447 load_extensions: A list of unpacked extensions paths to load on start. | 446 load_extensions: A list of unpacked extensions paths to load on start. |
448 load_apps: A list of unpacked apps to load on start. | 447 load_apps: A list of unpacked apps to load on start. |
449 start_path: The path relative to the current directory to point the browser | 448 start_path: The path relative to the current directory to point the browser |
450 at on startup. | 449 at on startup. |
451 """ | 450 """ |
452 # Ensure all extension / app paths are absolute. | 451 # Ensure all extension / app paths are absolute. |
453 load_extensions = [os.path.abspath(os.path.expanduser(i)) | 452 load_extensions = [os.path.abspath(os.path.expanduser(i)) |
454 for i in load_extensions] | 453 for i in load_extensions] |
455 load_apps = [os.path.abspath(os.path.expanduser(i)) | 454 load_apps = [os.path.abspath(os.path.expanduser(i)) for i in load_apps] |
456 for i in load_apps] | |
457 | 455 |
458 # Add in the chrome_test extension and compute its id. | 456 # Add in the chrome_test extension and compute its id. |
459 load_extensions += [TESTING_EXTENSION, TESTING_TCP_APP] | 457 load_extensions += [TESTING_EXTENSION, TESTING_TCP_APP] |
460 testing_id = ChromeAppIdFromPath(TESTING_EXTENSION) | 458 testing_id = ChromeAppIdFromPath(TESTING_EXTENSION) |
461 | 459 |
462 s = ChromeTestServer(('', 0), ChromeTestHandler) | 460 s = ChromeTestServer(('', 0), ChromeTestHandler) |
463 for root in roots: | 461 for root in roots: |
464 s.AddRoot(root) | 462 s.AddRoot(root) |
465 s.SetFilterString(filter_string) | 463 s.SetFilterString(filter_string) |
466 | 464 |
(...skipping 10 matching lines...) Expand all Loading... |
477 try: | 475 try: |
478 work_dir = tempfile.mkdtemp(prefix='chrome_test_', suffix='.tmp') | 476 work_dir = tempfile.mkdtemp(prefix='chrome_test_', suffix='.tmp') |
479 work_dir = os.path.abspath(work_dir) | 477 work_dir = os.path.abspath(work_dir) |
480 logging.info('Created work area in %s' % work_dir) | 478 logging.info('Created work area in %s' % work_dir) |
481 try: | 479 try: |
482 thread = threading.Thread(target=Target) | 480 thread = threading.Thread(target=Target) |
483 thread.start() | 481 thread.start() |
484 | 482 |
485 cmd = [] | 483 cmd = [] |
486 if sys.platform.startswith('linux') and use_xvfb: | 484 if sys.platform.startswith('linux') and use_xvfb: |
487 cmd += ['xvfb-run', '--auto-servernum', | 485 cmd += ['xvfb-run', '--auto-servernum', '-s', |
488 '-s', '-screen 0 1024x768x24 -ac'] | 486 '-screen 0 1024x768x24 -ac'] |
489 cmd += [chrome_path] | 487 cmd += [chrome_path] |
490 cmd += ['--user-data-dir=' + work_dir] | 488 cmd += ['--user-data-dir=' + work_dir] |
491 # We want to pin the pnacl component to the one that we downloaded. | 489 # We want to pin the pnacl component to the one that we downloaded. |
492 # This allows us to test features of the pnacl translator that are | 490 # This allows us to test features of the pnacl translator that are |
493 # not yet in the public component. | 491 # not yet in the public component. |
494 cmd += ['--disable-component-update'] | 492 cmd += ['--disable-component-update'] |
495 # Pass testing extension id in user agent to make it widely available. | 493 # Pass testing extension id in user agent to make it widely available. |
496 # TODO(bradnelson): Drop this when hterm is fixed. | 494 # TODO(bradnelson): Drop this when hterm is fixed. |
497 # Hterm currently expects "Chrome/[0-9][0-9]" in the User Agent and | 495 # Hterm currently expects "Chrome/[0-9][0-9]" in the User Agent and |
498 # faults without it. Using "Chrome/34" so that it goes down one of the | 496 # faults without it. Using "Chrome/34" so that it goes down one of the |
(...skipping 10 matching lines...) Expand all Loading... |
509 if len(load_apps) != 0: | 507 if len(load_apps) != 0: |
510 cmd += ['--load-and-launch-app=' + ','.join(load_apps)] | 508 cmd += ['--load-and-launch-app=' + ','.join(load_apps)] |
511 cmd += [start_url] | 509 cmd += [start_url] |
512 | 510 |
513 def ProcessGroup(): | 511 def ProcessGroup(): |
514 if sys.platform != 'win32': | 512 if sys.platform != 'win32': |
515 # On non-windows platforms, start a new process group so that we can | 513 # On non-windows platforms, start a new process group so that we can |
516 # be certain we bring down Chrome on a timeout. | 514 # be certain we bring down Chrome on a timeout. |
517 os.setpgid(0, 0) | 515 os.setpgid(0, 0) |
518 | 516 |
519 p = subprocess.Popen( | 517 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, |
520 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, | 518 stderr=subprocess.STDOUT, preexec_fn=ProcessGroup) |
521 preexec_fn=ProcessGroup) | |
522 logging.info('Started chrome with command line: %s' % (' '.join(cmd))) | 519 logging.info('Started chrome with command line: %s' % (' '.join(cmd))) |
523 stdout, _, returncode = CommunicateWithTimeout(p, timeout=timeout) | 520 stdout, _, returncode = CommunicateWithTimeout(p, timeout=timeout) |
524 if logging.getLogger().isEnabledFor(logging.DEBUG): | 521 if logging.getLogger().isEnabledFor(logging.DEBUG): |
525 sys.stdout.write('\n[[[ STDOUT ]]]\n') | 522 sys.stdout.write('\n[[[ STDOUT ]]]\n') |
526 sys.stdout.write('-' * 70 + '\n') | 523 sys.stdout.write('-' * 70 + '\n') |
527 sys.stdout.write(stdout) | 524 sys.stdout.write(stdout) |
528 sys.stdout.write('\n' + '-' * 70 + '\n') | 525 sys.stdout.write('\n' + '-' * 70 + '\n') |
529 logging.info('Chrome exited with return code %d' % returncode) | 526 logging.info('Chrome exited with return code %d' % returncode) |
530 finally: | 527 finally: |
531 try: | 528 try: |
532 with contextlib.closing(urllib2.urlopen(quit_url)) as stream: | 529 with contextlib.closing(urllib2.urlopen(quit_url)) as stream: |
533 stream.read() | 530 stream.read() |
534 except Exception: | 531 except Exception: |
535 pass | 532 pass |
536 thread.join() | 533 thread.join() |
537 logging.info('Shutdown web server.') | 534 logging.info('Shutdown web server.') |
538 finally: | 535 finally: |
539 shutil.rmtree(work_dir) | 536 shutil.rmtree(work_dir) |
540 logging.info('Removed %s' % work_dir) | 537 logging.info('Removed %s' % work_dir) |
541 logging.info('Done.') | 538 logging.info('Done.') |
542 | 539 |
543 if returncode == RETURNCODE_KILL: | 540 if returncode == RETURNCODE_KILL: |
544 print '[ TIMEOUT ] Timed out, ran %d tests, %d failed.' % ( | 541 print '[ TIMEOUT ] Timed out, ran %d tests, %d failed.' % ( |
545 len(s.tests), len(s.failed_tests)) | 542 len(s.tests), len(s.failed_tests) |
| 543 ) |
546 sys.exit(1) | 544 sys.exit(1) |
547 elif s.expected_test_count is None: | 545 elif s.expected_test_count is None: |
548 print ('[ XXXXXXXX ] Expected test count never emitted.') | 546 print('[ XXXXXXXX ] Expected test count never emitted.') |
549 sys.exit(1) | 547 sys.exit(1) |
550 elif s.test_results != s.expected_test_count: | 548 elif s.test_results != s.expected_test_count: |
551 print ('[ XXXXXXXX ] ' | 549 print('[ XXXXXXXX ] ' |
552 'Expected %d tests, but only %d had results, with %d failures.' % ( | 550 'Expected %d tests, but only %d had results, with %d failures.' % ( |
553 s.expected_test_count, s.test_results, len(s.failed_tests))) | 551 s.expected_test_count, s.test_results, len(s.failed_tests) |
| 552 )) |
554 sys.exit(1) | 553 sys.exit(1) |
555 elif s.result != 0: | 554 elif s.result != 0: |
556 print '[ Failures ] Ran %d tests, %d failed.' % ( | 555 print '[ Failures ] Ran %d tests, %d failed.' % (len(s.tests), |
557 len(s.tests), len(s.failed_tests)) | 556 len(s.failed_tests)) |
558 sys.exit(1) | 557 sys.exit(1) |
559 else: | 558 else: |
560 print '[ Success! ] Ran %d tests.' % len(s.tests) | 559 print '[ Success! ] Ran %d tests.' % len(s.tests) |
561 | 560 |
562 | 561 |
563 def Main(argv): | 562 def Main(argv): |
564 """Main method to invoke in test harness programs. | 563 """Main method to invoke in test harness programs. |
565 Args: | 564 Args: |
566 argv: Command line options controlling what to run. | 565 argv: Command line options controlling what to run. |
567 See --help. | 566 See --help. |
568 NOTE: Ends the process with sys.exit(1) on failure. | 567 NOTE: Ends the process with sys.exit(1) on failure. |
569 """ | 568 """ |
570 parser = argparse.ArgumentParser(description=__doc__) | 569 parser = argparse.ArgumentParser(description=__doc__) |
571 parser.add_argument( | 570 parser.add_argument('start_path', metavar='START_PATH', |
572 'start_path', metavar='START_PATH', | 571 help='location in which to run tests') |
573 help='location in which to run tests') | 572 parser.add_argument('-x', '--xvfb', action='store_true', |
574 parser.add_argument( | 573 help='Run Chrome thru xvfb on Linux.') |
575 '-x', '--xvfb', action='store_true', | 574 parser.add_argument('-a', '--arch', default='x86_64', |
576 help='Run Chrome thru xvfb on Linux.') | 575 help='Chrome architecture: i686 / x86_64.') |
577 parser.add_argument( | 576 parser.add_argument('-v', '--verbose', default=0, action='count', |
578 '-a', '--arch', default='x86_64', | 577 help='Emit verbose output, use twice for more.') |
579 help='Chrome architecture: i686 / x86_64.') | 578 parser.add_argument('-t', '--timeout', default=30, type=float, |
580 parser.add_argument( | 579 help='Timeout for all tests (in seconds).') |
581 '-v', '--verbose', default=0, action='count', | 580 parser.add_argument('-C', '--chdir', default=[], action='append', |
582 help='Emit verbose output, use twice for more.') | 581 help='Add a root directory.') |
583 parser.add_argument( | 582 parser.add_argument('--load-extension', default=[], action='append', |
584 '-t', '--timeout', default=30, type=float, | 583 help='Add an extension to load on start.') |
585 help='Timeout for all tests (in seconds).') | 584 parser.add_argument('--load-and-launch-app', default=[], action='append', |
586 parser.add_argument( | 585 help='Add an app to load on start.') |
587 '-C', '--chdir', default=[], action='append', | 586 parser.add_argument('--unlimited-storage', default=False, action='store_true', |
588 help='Add a root directory.') | 587 help='Allow unlimited storage.') |
589 parser.add_argument( | 588 parser.add_argument('--enable-nacl', default=False, action='store_true', |
590 '--load-extension', default=[], action='append', | 589 help='Enable NaCl generally.') |
591 help='Add an extension to load on start.') | 590 parser.add_argument('--enable-nacl-debug', default=False, action='store_true', |
592 parser.add_argument( | 591 help='Enable NaCl debugging.') |
593 '--load-and-launch-app', default=[], action='append', | 592 parser.add_argument('-f', '--filter', default='*', help='Filter on tests.') |
594 help='Add an app to load on start.') | |
595 parser.add_argument( | |
596 '--unlimited-storage', default=False, action='store_true', | |
597 help='Allow unlimited storage.') | |
598 parser.add_argument( | |
599 '--enable-nacl', default=False, action='store_true', | |
600 help='Enable NaCl generally.') | |
601 parser.add_argument( | |
602 '--enable-nacl-debug', default=False, action='store_true', | |
603 help='Enable NaCl debugging.') | |
604 parser.add_argument( | |
605 '-f', '--filter', default='*', | |
606 help='Filter on tests.') | |
607 parser.add_argument( | 593 parser.add_argument( |
608 '-p', '--param', default=[], action='append', | 594 '-p', '--param', default=[], action='append', |
609 help='Add a parameter to the end of the url, = separated.') | 595 help='Add a parameter to the end of the url, = separated.') |
610 options = parser.parse_args(argv) | 596 options = parser.parse_args(argv) |
611 | 597 |
612 if options.param: | 598 if options.param: |
613 params = {} | 599 params = {} |
614 params['SYS_ARCH'] = options.arch | 600 params['SYS_ARCH'] = options.arch |
615 for param in options.param: | 601 for param in options.param: |
616 key, value = param.split('=', 1) | 602 key, value = param.split('=', 1) |
617 params[key] = value | 603 params[key] = value |
618 options.start_path += '?' + urllib.urlencode(params) | 604 options.start_path += '?' + urllib.urlencode(params) |
619 | 605 |
620 if options.verbose > 1: | 606 if options.verbose > 1: |
621 logging.getLogger().setLevel(logging.DEBUG) | 607 logging.getLogger().setLevel(logging.DEBUG) |
622 elif options.verbose > 0: | 608 elif options.verbose > 0: |
623 logging.getLogger().setLevel(logging.INFO) | 609 logging.getLogger().setLevel(logging.INFO) |
624 else: | 610 else: |
625 logging.getLogger().setLevel(logging.WARNING) | 611 logging.getLogger().setLevel(logging.WARNING) |
626 logging.basicConfig( | 612 logging.basicConfig(format='%(asctime)-15s %(levelname)s: %(message)s', |
627 format='%(asctime)-15s %(levelname)s: %(message)s', | 613 datefmt='%Y-%m-%d %H:%M:%S') |
628 datefmt='%Y-%m-%d %H:%M:%S') | |
629 if not options.chdir: | 614 if not options.chdir: |
630 options.chdir.append('.') | 615 options.chdir.append('.') |
631 | 616 |
632 if sys.platform.startswith('linux'): | 617 if sys.platform.startswith('linux'): |
633 default_sandbox_locations = [ | 618 default_sandbox_locations = [ |
634 '/usr/local/sbin/chrome-devel-sandbox', | 619 '/usr/local/sbin/chrome-devel-sandbox', |
635 '/opt/chromium/chrome_sandbox', | 620 '/opt/chromium/chrome_sandbox', |
636 '/opt/google/chrome-beta/chrome-sandbox' | 621 '/opt/google/chrome-beta/chrome-sandbox' |
637 ] | 622 ] |
638 if 'CHROME_DEVEL_SANDBOX' not in os.environ: | 623 if 'CHROME_DEVEL_SANDBOX' not in os.environ: |
(...skipping 16 matching lines...) Expand all Loading... |
655 filter_string=options.filter, | 640 filter_string=options.filter, |
656 roots=options.chdir, | 641 roots=options.chdir, |
657 use_xvfb=options.xvfb, | 642 use_xvfb=options.xvfb, |
658 unlimited_storage=options.unlimited_storage, | 643 unlimited_storage=options.unlimited_storage, |
659 enable_nacl=options.enable_nacl, | 644 enable_nacl=options.enable_nacl, |
660 enable_nacl_debug=options.enable_nacl_debug, | 645 enable_nacl_debug=options.enable_nacl_debug, |
661 load_extensions=options.load_extension, | 646 load_extensions=options.load_extension, |
662 load_apps=options.load_and_launch_app, | 647 load_apps=options.load_and_launch_app, |
663 start_path=options.start_path) | 648 start_path=options.start_path) |
664 sys.exit(0) | 649 sys.exit(0) |
OLD | NEW |