1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import atexit 6import logging 7import os 8import urllib2 9import urlparse 10 11try: 12 from selenium import webdriver 13except ImportError: 14 # Ignore import error, as this can happen when builder tries to call the 15 # setup method of test that imports chromedriver. 16 logging.error('selenium module failed to be imported.') 17 pass 18 19from autotest_lib.client.bin import utils 20from autotest_lib.client.common_lib.cros import chrome 21 22CHROMEDRIVER_EXE_PATH = '/usr/local/chromedriver/chromedriver' 23X_SERVER_DISPLAY = ':0' 24X_AUTHORITY = '/home/chronos/.Xauthority' 25 26 27class chromedriver(object): 28 """Wrapper class, a context manager type, for tests to use Chrome Driver.""" 29 30 def __init__(self, extra_chrome_flags=[], subtract_extra_chrome_flags=[], 31 extension_paths=[], username=None, password=None, 32 server_port=None, skip_cleanup=False, url_base=None, 33 extra_chromedriver_args=None, gaia_login=False, 34 disable_default_apps=True, dont_override_profile=False, *args, 35 **kwargs): 36 """Initialize. 37 38 @param extra_chrome_flags: Extra chrome flags to pass to chrome, if any. 39 @param subtract_extra_chrome_flags: Remove default flags passed to 40 chrome by chromedriver, if any. 41 @param extension_paths: A list of paths to unzipped extensions. Note 42 that paths to crx files won't work. 43 @param username: Log in using this username instead of the default. 44 @param password: Log in using this password instead of the default. 45 @param server_port: Port number for the chromedriver server. If None, 46 an available port is chosen at random. 47 @param skip_cleanup: If True, leave the server and browser running 48 so that remote tests can run after this script 49 ends. Default is False. 50 @param url_base: Optional base url for chromedriver. 51 @param extra_chromedriver_args: List of extra arguments to forward to 52 the chromedriver binary, if any. 53 @param gaia_login: Logs in to real gaia. 54 @param disable_default_apps: For tests that exercise default apps. 55 @param dont_override_profile: Don't delete cryptohome before login. 56 Telemetry will output a warning with this 57 option. 58 """ 59 self._cleanup = not skip_cleanup 60 assert os.geteuid() == 0, 'Need superuser privileges' 61 62 # When ChromeDriver starts Chrome on other platforms (Linux, Windows, 63 # etc.), it accepts flag inputs of the form "--flag_name" or 64 # "flag_name". Before starting Chrome with those flags, ChromeDriver 65 # reformats them all to "--flag_name". This behavior is copied 66 # to ChromeOS for consistency across platforms. 67 fixed_extra_chrome_flags = [ 68 f if f.startswith('--') else '--%s' % f for f in extra_chrome_flags] 69 70 # Log in with telemetry 71 self._chrome = chrome.Chrome(extension_paths=extension_paths, 72 username=username, 73 password=password, 74 extra_browser_args=fixed_extra_chrome_flags, 75 gaia_login=gaia_login, 76 disable_default_apps=disable_default_apps, 77 dont_override_profile=dont_override_profile 78 ) 79 self._browser = self._chrome.browser 80 # Close all tabs owned and opened by Telemetry, as these cannot be 81 # transferred to ChromeDriver. 82 self._browser.tabs[0].Close() 83 84 # Start ChromeDriver server 85 self._server = chromedriver_server(CHROMEDRIVER_EXE_PATH, 86 port=server_port, 87 skip_cleanup=skip_cleanup, 88 url_base=url_base, 89 extra_args=extra_chromedriver_args) 90 91 # Open a new tab using Chrome remote debugging. ChromeDriver expects 92 # a tab opened for remote to work. Tabs opened using Telemetry will be 93 # owned by Telemetry, and will be inaccessible to ChromeDriver. 94 urllib2.urlopen('http://localhost:%i/json/new' % 95 utils.get_chrome_remote_debugging_port()) 96 97 chromeOptions = {'debuggerAddress': 98 ('localhost:%d' % 99 utils.get_chrome_remote_debugging_port())} 100 capabilities = {'chromeOptions':chromeOptions} 101 # Handle to chromedriver, for chrome automation. 102 try: 103 self.driver = webdriver.Remote(command_executor=self._server.url, 104 desired_capabilities=capabilities) 105 except NameError: 106 logging.error('selenium module failed to be imported.') 107 raise 108 109 110 def __enter__(self): 111 return self 112 113 114 def __exit__(self, *args): 115 """Clean up after running the test. 116 117 """ 118 if hasattr(self, 'driver') and self.driver: 119 self.driver.close() 120 del self.driver 121 122 if not hasattr(self, '_cleanup') or self._cleanup: 123 if hasattr(self, '_server') and self._server: 124 self._server.close() 125 del self._server 126 127 if hasattr(self, '_browser') and self._browser: 128 self._browser.Close() 129 del self._browser 130 131 def get_extension(self, extension_path): 132 """Gets an extension by proxying to the browser. 133 134 @param extension_path: Path to the extension loaded in the browser. 135 136 @return: A telemetry extension object representing the extension. 137 """ 138 return self._chrome.get_extension(extension_path) 139 140 141 @property 142 def chrome_instance(self): 143 """ The chrome instance used by this chrome driver instance. """ 144 return self._chrome 145 146 147class chromedriver_server(object): 148 """A running ChromeDriver server. 149 150 This code is migrated from chrome: 151 src/chrome/test/chromedriver/server/server.py 152 """ 153 154 def __init__(self, exe_path, port=None, skip_cleanup=False, 155 url_base=None, extra_args=None): 156 """Starts the ChromeDriver server and waits for it to be ready. 157 158 Args: 159 exe_path: path to the ChromeDriver executable 160 port: server port. If None, an available port is chosen at random. 161 skip_cleanup: If True, leave the server running so that remote 162 tests can run after this script ends. Default is 163 False. 164 url_base: Optional base url for chromedriver. 165 extra_args: List of extra arguments to forward to the chromedriver 166 binary, if any. 167 Raises: 168 RuntimeError if ChromeDriver fails to start 169 """ 170 if not os.path.exists(exe_path): 171 raise RuntimeError('ChromeDriver exe not found at: ' + exe_path) 172 173 chromedriver_args = [exe_path] 174 if port: 175 # Allow remote connections if a port was specified 176 chromedriver_args.append('--whitelisted-ips') 177 else: 178 port = utils.get_unused_port() 179 chromedriver_args.append('--port=%d' % port) 180 181 self.url = 'http://localhost:%d' % port 182 if url_base: 183 chromedriver_args.append('--url-base=%s' % url_base) 184 self.url = urlparse.urljoin(self.url, url_base) 185 186 if extra_args: 187 chromedriver_args.extend(extra_args) 188 189 # TODO(ihf): Remove references to X after M45. 190 # Chromedriver will look for an X server running on the display 191 # specified through the DISPLAY environment variable. 192 os.environ['DISPLAY'] = X_SERVER_DISPLAY 193 os.environ['XAUTHORITY'] = X_AUTHORITY 194 195 self.bg_job = utils.BgJob(chromedriver_args, stderr_level=logging.DEBUG) 196 if self.bg_job is None: 197 raise RuntimeError('ChromeDriver server cannot be started') 198 199 try: 200 timeout_msg = 'Timeout on waiting for ChromeDriver to start.' 201 utils.poll_for_condition(self.is_running, 202 exception=utils.TimeoutError(timeout_msg), 203 timeout=10, 204 sleep_interval=.1) 205 except utils.TimeoutError: 206 self.close_bgjob() 207 raise RuntimeError('ChromeDriver server did not start') 208 209 logging.debug('Chrome Driver server is up and listening at port %d.', 210 port) 211 if not skip_cleanup: 212 atexit.register(self.close) 213 214 215 def is_running(self): 216 """Returns whether the server is up and running.""" 217 try: 218 urllib2.urlopen(self.url + '/status') 219 return True 220 except urllib2.URLError as e: 221 return False 222 223 224 def close_bgjob(self): 225 """Close background job and log stdout and stderr.""" 226 utils.nuke_subprocess(self.bg_job.sp) 227 utils.join_bg_jobs([self.bg_job], timeout=1) 228 result = self.bg_job.result 229 if result.stdout or result.stderr: 230 logging.info('stdout of Chrome Driver:\n%s', result.stdout) 231 logging.error('stderr of Chrome Driver:\n%s', result.stderr) 232 233 234 def close(self): 235 """Kills the ChromeDriver server, if it is running.""" 236 if self.bg_job is None: 237 return 238 239 try: 240 urllib2.urlopen(self.url + '/shutdown', timeout=10).close() 241 except: 242 pass 243 244 self.close_bgjob() 245