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 5 import atexit 6 import logging 7 import os 8 import urllib2 9 import urlparse 10 11 try: 12 from selenium import webdriver 13 except 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 19 from autotest_lib.client.bin import utils 20 from autotest_lib.client.common_lib.cros import chrome 21 22 CHROMEDRIVER_EXE_PATH = '/usr/local/chromedriver/chromedriver' 23 X_SERVER_DISPLAY = ':0' 24 X_AUTHORITY = '/home/chronos/.Xauthority' 25 26 27 class 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 # Log in with telemetry 63 self._chrome = chrome.Chrome(extension_paths=extension_paths, 64 username=username, 65 password=password, 66 extra_browser_args=extra_chrome_flags, 67 gaia_login=gaia_login, 68 disable_default_apps=disable_default_apps, 69 dont_override_profile=dont_override_profile 70 ) 71 self._browser = self._chrome.browser 72 # Close all tabs owned and opened by Telemetry, as these cannot be 73 # transferred to ChromeDriver. 74 self._browser.tabs[0].Close() 75 76 # Start ChromeDriver server 77 self._server = chromedriver_server(CHROMEDRIVER_EXE_PATH, 78 port=server_port, 79 skip_cleanup=skip_cleanup, 80 url_base=url_base, 81 extra_args=extra_chromedriver_args) 82 83 # Open a new tab using Chrome remote debugging. ChromeDriver expects 84 # a tab opened for remote to work. Tabs opened using Telemetry will be 85 # owned by Telemetry, and will be inaccessible to ChromeDriver. 86 urllib2.urlopen('http://localhost:%i/json/new' % 87 utils.get_chrome_remote_debugging_port()) 88 89 chromeOptions = {'debuggerAddress': 90 ('localhost:%d' % 91 utils.get_chrome_remote_debugging_port())} 92 capabilities = {'chromeOptions':chromeOptions} 93 # Handle to chromedriver, for chrome automation. 94 try: 95 self.driver = webdriver.Remote(command_executor=self._server.url, 96 desired_capabilities=capabilities) 97 except NameError: 98 logging.error('selenium module failed to be imported.') 99 raise 100 101 102 def __enter__(self): 103 return self 104 105 106 def __exit__(self, *args): 107 """Clean up after running the test. 108 109 """ 110 if hasattr(self, 'driver') and self.driver: 111 self.driver.close() 112 del self.driver 113 114 if not hasattr(self, '_cleanup') or self._cleanup: 115 if hasattr(self, '_server') and self._server: 116 self._server.close() 117 del self._server 118 119 if hasattr(self, '_browser') and self._browser: 120 self._browser.Close() 121 del self._browser 122 123 def get_extension(self, extension_path): 124 """Gets an extension by proxying to the browser. 125 126 @param extension_path: Path to the extension loaded in the browser. 127 128 @return: A telemetry extension object representing the extension. 129 """ 130 return self._chrome.get_extension(extension_path) 131 132 133 @property 134 def chrome_instance(self): 135 """ The chrome instance used by this chrome driver instance. """ 136 return self._chrome 137 138 139 class chromedriver_server(object): 140 """A running ChromeDriver server. 141 142 This code is migrated from chrome: 143 src/chrome/test/chromedriver/server/server.py 144 """ 145 146 def __init__(self, exe_path, port=None, skip_cleanup=False, 147 url_base=None, extra_args=None): 148 """Starts the ChromeDriver server and waits for it to be ready. 149 150 Args: 151 exe_path: path to the ChromeDriver executable 152 port: server port. If None, an available port is chosen at random. 153 skip_cleanup: If True, leave the server running so that remote 154 tests can run after this script ends. Default is 155 False. 156 url_base: Optional base url for chromedriver. 157 extra_args: List of extra arguments to forward to the chromedriver 158 binary, if any. 159 Raises: 160 RuntimeError if ChromeDriver fails to start 161 """ 162 if not os.path.exists(exe_path): 163 raise RuntimeError('ChromeDriver exe not found at: ' + exe_path) 164 165 chromedriver_args = [exe_path] 166 if port: 167 # Allow remote connections if a port was specified 168 chromedriver_args.append('--whitelisted-ips') 169 else: 170 port = utils.get_unused_port() 171 chromedriver_args.append('--port=%d' % port) 172 173 self.url = 'http://localhost:%d' % port 174 if url_base: 175 chromedriver_args.append('--url-base=%s' % url_base) 176 self.url = urlparse.urljoin(self.url, url_base) 177 178 if extra_args: 179 chromedriver_args.extend(extra_args) 180 181 # TODO(ihf): Remove references to X after M45. 182 # Chromedriver will look for an X server running on the display 183 # specified through the DISPLAY environment variable. 184 os.environ['DISPLAY'] = X_SERVER_DISPLAY 185 os.environ['XAUTHORITY'] = X_AUTHORITY 186 187 self.bg_job = utils.BgJob(chromedriver_args, stderr_level=logging.DEBUG) 188 if self.bg_job is None: 189 raise RuntimeError('ChromeDriver server cannot be started') 190 191 try: 192 timeout_msg = 'Timeout on waiting for ChromeDriver to start.' 193 utils.poll_for_condition(self.is_running, 194 exception=utils.TimeoutError(timeout_msg), 195 timeout=10, 196 sleep_interval=.1) 197 except utils.TimeoutError: 198 self.close_bgjob() 199 raise RuntimeError('ChromeDriver server did not start') 200 201 logging.debug('Chrome Driver server is up and listening at port %d.', 202 port) 203 if not skip_cleanup: 204 atexit.register(self.close) 205 206 207 def is_running(self): 208 """Returns whether the server is up and running.""" 209 try: 210 urllib2.urlopen(self.url + '/status') 211 return True 212 except urllib2.URLError as e: 213 return False 214 215 216 def close_bgjob(self): 217 """Close background job and log stdout and stderr.""" 218 utils.nuke_subprocess(self.bg_job.sp) 219 utils.join_bg_jobs([self.bg_job], timeout=1) 220 result = self.bg_job.result 221 if result.stdout or result.stderr: 222 logging.info('stdout of Chrome Driver:\n%s', result.stdout) 223 logging.error('stderr of Chrome Driver:\n%s', result.stderr) 224 225 226 def close(self): 227 """Kills the ChromeDriver server, if it is running.""" 228 if self.bg_job is None: 229 return 230 231 try: 232 urllib2.urlopen(self.url + '/shutdown', timeout=10).close() 233 except: 234 pass 235 236 self.close_bgjob() 237