| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 | 2 |
| 3 # Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | 3 # Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file |
| 4 # for details. All rights reserved. Use of this source code is governed by a | 4 # for details. All rights reserved. Use of this source code is governed by a |
| 5 # BSD-style license that can be found in the LICENSE file. | 5 # BSD-style license that can be found in the LICENSE file. |
| 6 # | 6 # |
| 7 | 7 |
| 8 """Script to actually open a browser and perform the test, and reports back with | 8 """Script to actually open a browser and perform the test, and reports back with |
| 9 the result. | 9 the result. It uses Selenium WebDriver when possible for running the tests. It |
| 10 uses Selenium RC for Safari. |
| 11 |
| 12 If started with --batch this script runs a batch of in-browser tests in |
| 13 the same browser process. |
| 14 |
| 15 Normal mode: |
| 16 $ python run_selenium.py --browser=ff --timeout=60 path/to/test.html |
| 17 |
| 18 Exit code indicates pass or fail |
| 19 |
| 20 Batch mode: |
| 21 $ python run_selenium.py --batch |
| 22 stdin: --browser=ff --timeout=60 path/to/test.html |
| 23 stdout: >>> TEST PASS |
| 24 stdin: --browser=ff --timeout=60 path/to/test2.html |
| 25 stdout: >>> TEST FAIL |
| 26 stdin: --terminate |
| 27 $ |
| 10 """ | 28 """ |
| 11 | 29 |
| 12 import os | 30 import os |
| 13 import optparse | 31 import optparse |
| 14 import platform | 32 import platform |
| 15 import selenium | 33 import selenium |
| 16 from selenium.webdriver.support.ui import WebDriverWait | 34 from selenium.webdriver.support.ui import WebDriverWait |
| 17 import shutil | 35 import shutil |
| 18 import socket | 36 import socket |
| 19 import sys | 37 import sys |
| 20 import time | 38 import time |
| 39 import signal |
| 40 |
| 41 TIMEOUT_ERROR_MSG = 'FAIL (timeout)' |
| 21 | 42 |
| 22 def perf_test_done(driver): | 43 def perf_test_done(driver): |
| 23 """Checks if the performance test has completed.""" | 44 """Checks if the performance test has completed.""" |
| 24 return perf_test_done_helper(driver.page_source) | 45 return perf_test_done_helper(driver.page_source) |
| 25 | 46 |
| 26 def perf_test_done_helper(source): | 47 def perf_test_done_helper(source): |
| 27 """Tests to see if our performance test is done by printing a score.""" | 48 """Tests to see if our performance test is done by printing a score.""" |
| 28 #This code is written this way to work around a current instability in the | 49 #This code is written this way to work around a current instability in the |
| 29 # python webdriver bindings if you call driver.get_element_by_id. | 50 # python webdriver bindings if you call driver.get_element_by_id. |
| 30 #TODO(efortuna): Access these elements in a nicer way using DOM parser. | 51 #TODO(efortuna): Access these elements in a nicer way using DOM parser. |
| 31 string = '<div id="status">' | 52 string = '<div id="status">' |
| 32 index = source.find(string) | 53 index = source.find(string) |
| 33 end_index = source.find('</div>', index+1) | 54 end_index = source.find('</div>', index+1) |
| 34 source = source[index + len(string):end_index] | 55 source = source[index + len(string):end_index] |
| 35 return 'Score:' in source | 56 return 'Score:' in source |
| 36 | 57 |
| 37 def run_test_in_browser(browser, html_out, timeout, is_perf): | 58 def run_test_in_browser(browser, html_out, timeout, is_perf): |
| 38 """Run the desired test in the browser using Selenium 2.0 WebDriver syntax, | 59 """Run the desired test in the browser using Selenium 2.0 WebDriver syntax, |
| 39 and wait for the test to complete. This is the newer syntax, that currently | 60 and wait for the test to complete. This is the newer syntax, that currently |
| 40 supports Firefox, Chrome, IE, Opera (and some mobile browsers).""" | 61 supports Firefox, Chrome, IE, Opera (and some mobile browsers).""" |
| 41 browser.get("file://" + html_out) | 62 if isinstance(browser, selenium.selenium): |
| 63 return run_test_in_browser_selenium_rc(browser, html_out, timeout, is_perf) |
| 64 |
| 65 browser.get("file://" + html_out) |
| 42 source = '' | 66 source = '' |
| 43 try: | 67 try: |
| 44 if is_perf: | 68 if is_perf: |
| 45 # We're running a performance test. | 69 # We're running a performance test. |
| 46 element = WebDriverWait(browser, float(timeout)).until(perf_test_done) | 70 element = WebDriverWait(browser, float(timeout)).until(perf_test_done) |
| 47 else: | 71 else: |
| 48 element = WebDriverWait(browser, float(timeout)).until( | 72 element = WebDriverWait(browser, float(timeout)).until( |
| 49 lambda driver : ('PASS' in driver.page_source) or | 73 lambda driver : ('PASS' in driver.page_source) or |
| 50 ('FAIL' in driver.page_source)) | 74 ('FAIL' in driver.page_source)) |
| 51 source = browser.page_source | 75 source = browser.page_source |
| 52 except selenium.common.exceptions.TimeoutException: | 76 except selenium.common.exceptions.TimeoutException: |
| 53 source = 'FAIL (timeout)' | 77 source = TIMEOUT_ERROR_MSG |
| 54 finally: | |
| 55 # A timeout exception is thrown if nothing happens within the time limit. | |
| 56 if browser != 'chrome': | |
| 57 browser.close() | |
| 58 try: | |
| 59 browser.quit() | |
| 60 except selenium.common.exceptions.WebDriverException: | |
| 61 #TODO(efortuna): figure out why this crashes.... and avoid? | |
| 62 pass | |
| 63 return source | 78 return source |
| 64 | 79 |
| 65 def run_test_in_browser_selenium1(sel, html_out, timeout, is_perf): | 80 def run_test_in_browser_selenium_rc(sel, html_out, timeout, is_perf): |
| 66 """ Run the desired test in the browser using Selenium 1.0 syntax, and wait | 81 """ Run the desired test in the browser using Selenium 1.0 syntax, and wait |
| 67 for the test to complete. This is used for Safari, since it is not currently | 82 for the test to complete. This is used for Safari, since it is not currently |
| 68 supported on Selenium 2.0.""" | 83 supported on Selenium 2.0.""" |
| 69 sel.open('file://' + html_out) | 84 sel.open('file://' + html_out) |
| 70 source = sel.get_html_source() | 85 source = sel.get_html_source() |
| 71 def end_condition(source): | 86 def end_condition(source): |
| 72 return 'PASS' in source or 'FAIL' in source | 87 return 'PASS' in source or 'FAIL' in source |
| 73 if is_perf: | 88 if is_perf: |
| 74 end_condition = perf_test_done_helper | 89 end_condition = perf_test_done_helper |
| 75 | 90 |
| 76 elapsed = 0 | 91 elapsed = 0 |
| 77 while (not end_condition(source)) and elapsed <= timeout: | 92 while (not end_condition(source)) and elapsed <= timeout: |
| 78 sec = .25 | 93 sec = .25 |
| 79 time.sleep(sec) | 94 time.sleep(sec) |
| 80 elapsed += sec | 95 elapsed += sec |
| 81 source = sel.get_html_source() | 96 source = sel.get_html_source() |
| 82 sel.stop() | |
| 83 return source | 97 return source |
| 84 | 98 |
| 85 def parse_args(): | 99 def parse_args(args=None): |
| 86 parser = optparse.OptionParser() | 100 parser = optparse.OptionParser() |
| 87 parser.add_option('--out', dest='out', | 101 parser.add_option('--out', dest='out', |
| 88 help = 'The path for html output file that we will running our test from',
| 102 help = 'The path for html output file that we will running our test from', |
| 89 action = 'store', default = '') | 103 action = 'store', default = '') |
| 90 parser.add_option('--browser', dest='browser', | 104 parser.add_option('--browser', dest='browser', |
| 91 help = 'The browser type (default = chrome)', | 105 help = 'The browser type (default = chrome)', |
| 92 action = 'store', default = 'chrome') | 106 action = 'store', default = 'chrome') |
| 93 # TODO(efortuna): Put this back up to be more than the default timeout in | 107 # TODO(efortuna): Put this back up to be more than the default timeout in |
| 94 # test.dart. Right now it needs to be less than 60 so that when test.dart | 108 # test.dart. Right now it needs to be less than 60 so that when test.dart |
| 95 # times out, this script also closes the browser windows. | 109 # times out, this script also closes the browser windows. |
| 96 parser.add_option('--timeout', dest = 'timeout', | 110 parser.add_option('--timeout', dest = 'timeout', |
| 97 help = 'Amount of time (seconds) to wait before timeout', type = 'int', | 111 help = 'Amount of time (seconds) to wait before timeout', type = 'int', |
| 98 action = 'store', default=58) | 112 action = 'store', default=58) |
| 99 parser.add_option('--perf', dest = 'is_perf', | 113 parser.add_option('--perf', dest = 'is_perf', |
| 100 help = 'Add this flag if we are running a browser performance test', | 114 help = 'Add this flag if we are running a browser performance test', |
| 101 action = 'store_true', default=False) | 115 action = 'store_true', default=False) |
| 102 args, ignored = parser.parse_args() | 116 args, ignored = parser.parse_args(args=args) |
| 103 return args.out, args.browser, args.timeout, args.is_perf | 117 return args.out, args.browser, args.timeout, args.is_perf |
| 104 | 118 |
| 105 def Main(): | 119 def start_browser(browser, html_out): |
| 106 # Note: you need ChromeDriver *in your path* to run Chrome, in addition to | |
| 107 # installing Chrome. | |
| 108 browser = None | |
| 109 html_out, browser, timeout, is_perf = parse_args() | |
| 110 | |
| 111 if browser == 'chrome': | 120 if browser == 'chrome': |
| 112 browser = selenium.webdriver.Chrome() | 121 # Note: you need ChromeDriver *in your path* to run Chrome, in addition to |
| 122 # installing Chrome. Also note that the build bot runs have a different path |
| 123 # from a normal user -- check the build logs. |
| 124 return selenium.webdriver.Chrome() |
| 113 elif browser == 'ff': | 125 elif browser == 'ff': |
| 114 profile = selenium.webdriver.firefox.firefox_profile.FirefoxProfile() | 126 profile = selenium.webdriver.firefox.firefox_profile.FirefoxProfile() |
| 115 profile.set_preference('dom.max_script_run_time', 0) | 127 profile.set_preference('dom.max_script_run_time', 0) |
| 116 profile.set_preference('dom.max_chrome_script_run_time', 0) | 128 profile.set_preference('dom.max_chrome_script_run_time', 0) |
| 117 browser = selenium.webdriver.Firefox(firefox_profile=profile) | 129 return selenium.webdriver.Firefox(firefox_profile=profile) |
| 118 elif browser == 'ie' and platform.system() == 'Windows': | 130 elif browser == 'ie' and platform.system() == 'Windows': |
| 119 browser = selenium.webdriver.Ie() | 131 return selenium.webdriver.Ie() |
| 120 elif browser == 'safari' and platform.system() == 'Darwin': | 132 elif browser == 'safari' and platform.system() == 'Darwin': |
| 121 # TODO(efortuna): Ensure our preferences (no pop-up blocking) file is the | 133 # TODO(efortuna): Ensure our preferences (no pop-up blocking) file is the |
| 122 # same (Safari auto-deletes when it has too many "crashes," or in our case, | 134 # same (Safari auto-deletes when it has too many "crashes," or in our case, |
| 123 # timeouts). Come up with a less hacky way to do this. | 135 # timeouts). Come up with a less hacky way to do this. |
| 124 shutil.copy(os.path.dirname(__file__) + '/com.apple.Safari.plist', | 136 backup_safari_prefs = os.path.dirname(__file__) + '/com.apple.Safari.plist' |
| 125 '/Library/Preferences/com.apple.Safari.plist') | 137 if os.path.exists(backup_safari_prefs): |
| 138 shutil.copy(backup_safari_prefs, |
| 139 '/Library/Preferences/com.apple.Safari.plist') |
| 126 sel = selenium.selenium('localhost', 4444, "*safari", 'file://' + html_out) | 140 sel = selenium.selenium('localhost', 4444, "*safari", 'file://' + html_out) |
| 127 try: | 141 try: |
| 128 sel.start() | 142 sel.start() |
| 143 return sel |
| 129 except socket.error: | 144 except socket.error: |
| 130 print 'ERROR: Could not connect to Selenium RC server. Are you running' +\ | 145 print 'ERROR: Could not connect to Selenium RC server. Are you running' +\ |
| 131 ' java -jar selenium-server-standalone-2.15.0.jar? If not, start ' + \ | 146 ' java -jar selenium-server-standalone-2.15.0.jar? If not, start ' + \ |
| 132 'it before running this test.' | 147 'it before running this test.' |
| 133 return 1 | 148 sys.exit(1) |
| 134 else: | 149 else: |
| 135 raise Exception('Incompatible browser and platform combination.') | 150 raise Exception('Incompatible browser and platform combination.') |
| 136 source = '' | |
| 137 if browser == 'safari': | |
| 138 source = run_test_in_browser_selenium1(sel, html_out, timeout, is_perf) | |
| 139 else: | |
| 140 source = run_test_in_browser(browser, html_out, timeout, is_perf) | |
| 141 | 151 |
| 152 def close_browser(browser): |
| 153 if browser is None: |
| 154 return |
| 155 if isinstance(browser, selenium.selenium): |
| 156 browser.stop() |
| 157 return |
| 158 |
| 159 # A timeout exception is thrown if nothing happens within the time limit. |
| 160 if browser != 'chrome': |
| 161 browser.close() |
| 162 try: |
| 163 browser.quit() |
| 164 except selenium.common.exceptions.WebDriverException: |
| 165 # TODO(efortuna): Figure out why this crashes.... and avoid? |
| 166 pass |
| 167 |
| 168 def report_results(is_perf, source): |
| 142 if is_perf: | 169 if is_perf: |
| 143 # We're running a performance test. | 170 # We're running a performance test. |
| 144 print source | 171 print source |
| 145 if 'NaN' in source: | 172 if 'NaN' in source: |
| 146 return 1 | 173 return 1 |
| 147 else: | 174 else: |
| 148 return 0 | 175 return 0 |
| 149 else: | 176 else: |
| 150 # We're running a correctness test. Mark test as passing if all individual | 177 # We're running a correctness test. Mark test as passing if all individual |
| 151 # test cases pass. | 178 # test cases pass. |
| 152 if 'FAIL' not in source and 'PASS' in source: | 179 if 'FAIL' not in source and 'PASS' in source: |
| 153 print 'Content-Type: text/plain\nPASS' | 180 print 'Content-Type: text/plain\nPASS' |
| 154 return 0 | 181 return 0 |
| 155 else: | 182 else: |
| 156 #The hacky way to get document.getElementById('body').innerHTML for this | 183 #The hacky way to get document.getElementById('body').innerHTML for this |
| 157 # webpage, without the JavaScript. | 184 # webpage, without the JavaScript. |
| 158 #TODO(efortuna): Access these elements in a nicer way using DOM parser. | 185 #TODO(efortuna): Access these elements in a nicer way using DOM parser. |
| 159 index = source.find('<body>') | 186 index = source.find('<body>') |
| 160 index += len('<body>') | 187 index += len('<body>') |
| 161 end_index = source.find('<script') | 188 end_index = source.find('<script') |
| 162 print source[index : end_index] | 189 print unicode(source[index : end_index]).encode("utf-8") |
| 163 return 1 | 190 return 1 |
| 164 | 191 |
| 165 | 192 |
| 193 def run_batch_tests(): |
| 194 ''' |
| 195 Runs a batch of in-browser tests in the same browser process. Batching |
| 196 gives faster throughput and makes tests less subject to browser starting |
| 197 flakiness, issues with too many browser processes running, etc. |
| 198 |
| 199 When running this function, stdin/stdout is used to communicate with the test |
| 200 framework. See BatchRunnerProcess in test_runner.dart for the other side of |
| 201 this communication channel |
| 202 |
| 203 Example of usage: |
| 204 $ python run_selenium.py --batch |
| 205 stdin: --browser=ff --timeout=60 path/to/test.html |
| 206 stdout: >>> TEST PASS |
| 207 stdin: --browser=ff --timeout=60 path/to/test2.html |
| 208 stdout: >>> TEST FAIL |
| 209 stdin: --terminate |
| 210 $ |
| 211 ''' |
| 212 |
| 213 print '>>> BATCH START' |
| 214 browser = None |
| 215 current_browser_name = None |
| 216 |
| 217 # TODO(jmesserly): It'd be nice to shutdown gracefully in the event of a |
| 218 # SIGTERM. Unfortunately dart:io cannot send SIGTERM, see dartbug.com/1756. |
| 219 signal.signal(signal.SIGTERM, lambda number, frame: close_browser(browser)) |
| 220 |
| 221 try: |
| 222 while True: |
| 223 line = sys.stdin.readline() |
| 224 if line == '--terminate\n': |
| 225 break |
| 226 |
| 227 html_out, browser_name, timeout, is_perf = parse_args(line.split()) |
| 228 |
| 229 # Sanity checks that test.dart is passing flags we can handle. |
| 230 if is_perf: |
| 231 print 'Batch test runner not compatible with perf testing' |
| 232 return 1 |
| 233 if browser and current_browser_name != browser_name: |
| 234 print('Batch test runner got multiple browsers: %s and %s' |
| 235 % (current_browser_name, browser_name)) |
| 236 return 1 |
| 237 |
| 238 # Start the browser on the first run |
| 239 if browser is None: |
| 240 current_browser_name = browser_name |
| 241 browser = start_browser(browser_name, html_out) |
| 242 |
| 243 source = run_test_in_browser(browser, html_out, timeout, is_perf) |
| 244 |
| 245 # print one of: |
| 246 # >>> TEST {PASS, FAIL, OK, CRASH, FAIL, TIMEOUT} |
| 247 status = report_results(is_perf, source) |
| 248 if status == 0: |
| 249 print '>>> TEST PASS' |
| 250 elif source == TIMEOUT_ERROR_MSG: |
| 251 print '>>> TEST TIMEOUT' |
| 252 else: |
| 253 print '>>> TEST FAIL' |
| 254 sys.stdout.flush() |
| 255 finally: |
| 256 close_browser(browser) |
| 257 |
| 258 |
| 259 def main(args): |
| 260 # Run in batch mode if the --batch flag is passed. |
| 261 # TODO(jmesserly): reconcile with the existing args parsing |
| 262 if '--batch' in args: |
| 263 return run_batch_tests() |
| 264 |
| 265 # Run a single test |
| 266 html_out, browser_name, timeout, is_perf = parse_args() |
| 267 browser = start_browser(browser_name, html_out) |
| 268 |
| 269 try: |
| 270 output = run_test_in_browser(browser, html_out, timeout, is_perf) |
| 271 return report_results(is_perf, output) |
| 272 finally: |
| 273 close_browser(browser) |
| 274 |
| 166 if __name__ == "__main__": | 275 if __name__ == "__main__": |
| 167 sys.exit(Main()) | 276 sys.exit(main(sys.argv)) |
| OLD | NEW |