| #!/usr/bin/python |
| |
| # Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file |
| # for details. All rights reserved. Use of this source code is governed by a |
| # BSD-style license that can be found in the LICENSE file. |
| # |
| |
| """Script to actually open a browser and perform the test, and reports back with |
| the result. It uses Selenium WebDriver when possible for running the tests. It |
| uses Selenium RC for Safari. |
| |
| If started with --batch this script runs a batch of in-browser tests in |
| the same browser process. |
| |
| Normal mode: |
| $ python run_selenium.py --browser=ff --timeout=60 path/to/test.html |
| |
| Exit code indicates pass or fail |
| |
| Batch mode: |
| $ python run_selenium.py --batch |
| stdin: --browser=ff --timeout=60 path/to/test.html |
| stdout: >>> TEST PASS |
| stdin: --browser=ff --timeout=60 path/to/test2.html |
| stdout: >>> TEST FAIL |
| stdin: --terminate |
| $ |
| """ |
| |
| import os |
| import optparse |
| import platform |
| import selenium |
| from selenium.webdriver.common.desired_capabilities import DesiredCapabilities |
| from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver |
| from selenium.webdriver.support.ui import WebDriverWait |
| import shutil |
| import signal |
| import socket |
| import sys |
| import time |
| import urllib2 |
| import threading |
| |
| TIMEOUT_ERROR_MSG = 'FAIL (timeout)' |
| CRASH_ERROR_MSG = 'CRASH' |
| |
| def correctness_test_done(source): |
| """Checks if test has completed.""" |
| return ('PASS' in source) or ('FAIL' in source) |
| |
| def perf_test_done(source): |
| """Tests to see if our performance test is done by printing a score.""" |
| #This code is written this way to work around a current instability in the |
| # python webdriver bindings if you call driver.get_element_by_id. |
| #TODO(efortuna): Access these elements in a nicer way using DOM parser. |
| string = '<div id="status">' |
| index = source.find(string) |
| end_index = source.find('</div>', index+1) |
| source = source[index + len(string):end_index] |
| return 'Score:' in source |
| |
| # TODO(vsm): Ideally, this wouldn't live in this file. |
| CONFIGURATIONS = { |
| 'correctness': correctness_test_done, |
| 'perf': perf_test_done |
| } |
| |
| def run_test_in_browser(browser, html_out, timeout, mode, refresh): |
| """Run the desired test in the browser using Selenium 2.0 WebDriver syntax, |
| and wait for the test to complete. This is the newer syntax, that currently |
| supports Firefox, Chrome, IE, Opera (and some mobile browsers).""" |
| |
| if isinstance(browser, selenium.selenium): |
| return run_test_in_browser_selenium_rc(browser, html_out, timeout, mode, |
| refresh) |
| |
| browser.get(html_out) |
| if refresh: |
| browser.refresh() |
| try: |
| def pythonTimeout(): |
| # The builtin quit call for chrome will call close on the RemoteDriver |
| # which may hang. Explicitly call browser.service.stop() |
| if (type(browser) is selenium.webdriver.chrome.webdriver.WebDriver): |
| # Browser may be dead |
| try: |
| browser.service.stop() |
| except: |
| print("Trying to close browser that has already been closed") |
| # If the browser is crashing selenium may not time out. |
| # Explicitly catch this case with a python timer. |
| t = threading.Timer(timeout, pythonTimeout) |
| t.start() |
| test_done = CONFIGURATIONS[mode] |
| element = WebDriverWait(browser, float(timeout)).until( |
| lambda driver: test_done(driver.page_source)) |
| t.cancel() |
| return browser.page_source |
| except selenium.common.exceptions.TimeoutException: |
| return TIMEOUT_ERROR_MSG |
| except: |
| return CRASH_ERROR_MSG |
| |
| def run_test_in_browser_selenium_rc(sel, html_out, timeout, mode, refresh): |
| """ Run the desired test in the browser using Selenium 1.0 syntax, and wait |
| for the test to complete. This is used for Safari, since it is not currently |
| supported on Selenium 2.0.""" |
| sel.open(html_out) |
| if refresh: |
| sel.refresh() |
| source = sel.get_html_source() |
| end_condition = CONFIGURATIONS[mode] |
| |
| elapsed = 0 |
| while (not end_condition(source)) and elapsed <= timeout: |
| sec = .25 |
| time.sleep(sec) |
| elapsed += sec |
| source = sel.get_html_source() |
| return source |
| |
| def parse_args(args=None): |
| parser = optparse.OptionParser() |
| parser.add_option('--out', dest='out', |
| help = 'The path for html output file that we will running our test from', |
| action = 'store', default = '') |
| parser.add_option('--browser', dest='browser', |
| help = 'The browser type (default = chrome)', |
| action = 'store', default = 'chrome') |
| parser.add_option('--executable', dest='executable', |
| help = 'The browser executable path (only for browser=dartium)', |
| action = 'store', default = None) |
| # TODO(efortuna): Put this back up to be more than the default timeout in |
| # test.dart. Right now it needs to be less than 60 so that when test.dart |
| # times out, this script also closes the browser windows. |
| parser.add_option('--timeout', dest = 'timeout', |
| help = 'Amount of time (seconds) to wait before timeout', type = 'int', |
| action = 'store', default=58) |
| parser.add_option('--mode', dest = 'mode', |
| help = 'The type of test we are running', |
| action = 'store', default='correctness') |
| parser.add_option('--force-refresh', dest='refresh', |
| help='Force the browser to refresh before getting results from this test ' |
| '(used for browser multitests).', action='store_true', default=False) |
| args, _ = parser.parse_args(args=args) |
| args.out = args.out.strip('"') |
| if args.executable and args.browser != 'dartium': |
| print 'Executable path only supported when browser=dartium.' |
| sys.exit(1) |
| return (args.out, args.browser, args.executable, args.timeout, args.mode, |
| args.refresh) |
| |
| def print_server_error(): |
| """Provide the user an informative error message if we attempt to connect to |
| the Selenium remote control server, but cannot access it. Then exit the |
| program.""" |
| print ('ERROR: Could not connect to Selenium RC server. Are you running' |
| ' java -jar tools/testing/selenium-server-standalone-*.jar? If not, ' |
| 'start it before running this test.') |
| sys.exit(1) |
| |
| def start_browser(browser, executable_path, html_out): |
| if browser == 'chrome' or browser == 'dartium': |
| # Note: you need ChromeDriver *in your path* to run Chrome, in addition to |
| # installing Chrome. Also note that the build bot runs have a different path |
| # from a normal user -- check the build logs. |
| options = selenium.webdriver.chrome.options.Options() |
| if browser == 'dartium': |
| script_dir = os.path.dirname(os.path.abspath(__file__)) |
| dartium_dir = os.path.join(script_dir, '..', '..', 'client', 'tests', |
| 'dartium') |
| # enable ShadowDOM and style scoped for Dartium |
| options.add_argument('--enable-shadow-dom') |
| options.add_argument('--enable-style-scoped') |
| if executable_path is not None: |
| options.binary_location = executable_path |
| elif platform.system() == 'Windows': |
| options.binary_location = os.path.join(dartium_dir, 'chrome.exe') |
| elif platform.system() == 'Darwin': |
| options.binary_location = os.path.join(dartium_dir, 'Chromium.app', |
| 'Contents', 'MacOS', 'Chromium') |
| else: |
| options.binary_location = os.path.join(dartium_dir, 'chrome') |
| return selenium.webdriver.Chrome(chrome_options=options) |
| elif browser == 'ff': |
| script_dir = os.path.dirname(os.path.abspath(__file__)) |
| profile = selenium.webdriver.firefox.firefox_profile.FirefoxProfile() |
| profile.set_preference('dom.max_script_run_time', 0) |
| profile.set_preference('dom.max_chrome_script_run_time', 0) |
| profile.set_preference('app.update.auto', True) |
| profile.set_preference('app.update.enabled', True) |
| return selenium.webdriver.Firefox(firefox_profile=profile) |
| elif ((browser == 'ie9' or browser == 'ie10') and |
| platform.system() == 'Windows'): |
| return selenium.webdriver.Ie() |
| elif browser == 'safari' and platform.system() == 'Darwin': |
| # TODO(efortuna): Ensure our preferences (no pop-up blocking) file is the |
| # same (Safari auto-deletes when it has too many "crashes," or in our case, |
| # timeouts). Come up with a less hacky way to do this. |
| backup_safari_prefs = os.path.dirname(__file__) + '/com.apple.Safari.plist' |
| if os.path.exists(backup_safari_prefs): |
| shutil.copy(backup_safari_prefs, |
| '/Library/Preferences/com.apple.Safari.plist') |
| sel = selenium.selenium('localhost', 4444, "*safari", html_out) |
| try: |
| sel.start() |
| return sel |
| except socket.error: |
| print_server_error() |
| elif browser == 'opera': |
| try: |
| driver = RemoteWebDriver(desired_capabilities=DesiredCapabilities.OPERA) |
| # By default, Opera sets their script timeout (the amount of time they |
| # expect to hear back from the JavaScript file) to be 10 seconds. We just |
| # make it an impossibly large number so that it doesn't time out for this |
| # reason, so it behaves like all of the other browser drivers. |
| driver.set_script_timeout(9000) |
| # If the webpage contains document.onreadystatechanged = function() {...} |
| # page load event does not correctly get fired and caught (OperaDriver |
| # bug). This is a band-aid. |
| driver.set_page_load_timeout(1) |
| return driver |
| except urllib2.URLError: |
| print_server_error() |
| else: |
| raise Exception('Incompatible browser and platform combination.') |
| |
| def close_browser(browser): |
| if browser is None: |
| return |
| if isinstance(browser, selenium.selenium): |
| browser.stop() |
| return |
| |
| # A timeout exception is thrown if nothing happens within the time limit. |
| if (type(browser) is not selenium.webdriver.chrome.webdriver.WebDriver and |
| type(browser) is not selenium.webdriver.ie.webdriver.WebDriver): |
| browser.close() |
| |
| browser.quit() |
| |
| def report_results(mode, source, browser): |
| if mode != 'correctness': |
| # We're running a performance test. |
| print source.encode('utf8') |
| sys.stdout.flush() |
| if 'NaN' in source: |
| return 1 |
| else: |
| return 0 |
| else: |
| # We're running a correctness test. Mark test as passing if all individual |
| # test cases pass. |
| if 'FAIL' not in source and 'PASS' in source: |
| print 'Content-Type: text/plain\nPASS' |
| return 0 |
| else: |
| #The hacky way to get document.getElementById('body').innerHTML for this |
| # webpage, without the JavaScript. |
| #TODO(efortuna): Access these elements in a nicer way using DOM parser. |
| index = source.find('<body>') |
| index += len('<body>') |
| end_index = source.find('</body') |
| print unicode(source[index : end_index]).encode("utf-8") |
| return 1 |
| |
| |
| def run_batch_tests(): |
| ''' |
| Runs a batch of in-browser tests in the same browser process. Batching |
| gives faster throughput and makes tests less subject to browser starting |
| flakiness, issues with too many browser processes running, etc. |
| |
| When running this function, stdin/stdout is used to communicate with the test |
| framework. See BatchRunnerProcess in test_runner.dart for the other side of |
| this communication channel |
| |
| Example of usage: |
| $ python run_selenium.py --batch |
| stdin: --browser=ff --timeout=60 path/to/test.html |
| stdout: >>> TEST PASS |
| stdin: --browser=ff --timeout=60 path/to/test2.html |
| stdout: >>> TEST FAIL |
| stdin: --terminate |
| $ |
| ''' |
| |
| print '>>> BATCH START' |
| browser = None |
| current_browser_name = None |
| |
| # TODO(jmesserly): It'd be nice to shutdown gracefully in the event of a |
| # SIGTERM. Unfortunately dart:io cannot send SIGTERM, see dartbug.com/1756. |
| signal.signal(signal.SIGTERM, lambda number, frame: close_browser(browser)) |
| |
| try: |
| try: |
| while True: |
| line = sys.stdin.readline() |
| if line == '--terminate\n': |
| print("Terminating selenium driver") |
| break |
| |
| (html_out, browser_name, executable_path, |
| timeout, mode, refresh) = parse_args(line.split()) |
| |
| # Sanity checks that test.dart is passing flags we can handle. |
| if mode != 'correctness': |
| print 'Batch test runner not compatible with perf testing' |
| return 1 |
| if browser and current_browser_name != browser_name: |
| print('Batch test runner got multiple browsers: %s and %s' |
| % (current_browser_name, browser_name)) |
| return 1 |
| |
| # Start the browser on the first run |
| if browser is None: |
| current_browser_name = browser_name |
| browser = start_browser(browser_name, executable_path, html_out) |
| |
| source = run_test_in_browser(browser, html_out, timeout, mode, refresh) |
| |
| # Test is done. Write end token to stderr and flush. |
| sys.stderr.write('>>> EOF STDERR\n') |
| sys.stderr.flush() |
| |
| # print one of: |
| # >>> TEST {PASS, FAIL, OK, CRASH, FAIL, TIMEOUT} |
| status = report_results(mode, source, browser) |
| if status == 0: |
| print '>>> TEST PASS' |
| elif source == TIMEOUT_ERROR_MSG: |
| print '>>> TEST TIMEOUT' |
| elif source == CRASH_ERROR_MSG: |
| print '>>> TEST CRASH' |
| # The browser crashed, set the browser to None so that we will |
| # create a new instance on next iteration. |
| browser = None |
| else: |
| print '>>> TEST FAIL' |
| sys.stdout.flush() |
| except: |
| type, value, traceback = sys.exc_info() |
| print "run_selenium.py: Unexpected exception occured: " |
| print " type: ", type |
| print " value: ", value |
| print " traceback: ", traceback |
| raise |
| finally: |
| sys.stdin.close() |
| print("Closing browser"); |
| |
| def close_output_streams(): |
| sys.stdout.flush() |
| sys.stdout.close() |
| sys.stderr.flush() |
| sys.stderr.close() |
| |
| def close_and_exit(): |
| print("Timed out waiting for browser to close") |
| close_output_streams() |
| exit(1) |
| |
| timer = threading.Timer(5.0, close_and_exit) |
| timer.start() |
| try: |
| close_browser(browser) |
| timer.cancel() |
| finally: |
| close_output_streams() |
| |
| def main(args): |
| # Run in batch mode if the --batch flag is passed. |
| # TODO(jmesserly): reconcile with the existing args parsing |
| if '--batch' in args: |
| return run_batch_tests() |
| |
| # Run a single test |
| html_out, browser_name, executable_path, timeout, mode, refresh = parse_args() |
| browser = start_browser(browser_name, executable_path, html_out) |
| |
| try: |
| output = run_test_in_browser(browser, html_out, timeout, mode, refresh) |
| return report_results(mode, output, browser) |
| finally: |
| close_browser(browser) |
| |
| if __name__ == "__main__": |
| sys.exit(main(sys.argv)) |