blob: 7971f6cfb34144267c1573ad1b5c8c0b6400677a [file] [log] [blame]
Dan Shic1d263b2013-10-04 17:31:38 -07001# 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
Payton Turnage0b2becc2015-06-17 16:31:14 -07009import urlparse
Dan Shic1d263b2013-10-04 17:31:38 -070010
Dan Shi96c77cb2013-11-19 10:30:37 -080011try:
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
Dan Shic1d263b2013-10-04 17:31:38 -070018
Dan Shic1d263b2013-10-04 17:31:38 -070019from autotest_lib.client.bin import utils
20from autotest_lib.client.common_lib.cros import chrome
21
22CHROMEDRIVER_EXE_PATH = '/usr/local/chromedriver/chromedriver'
beeps47a51292013-11-20 08:55:23 -080023X_SERVER_DISPLAY = ':0'
24X_AUTHORITY = '/home/chronos/.Xauthority'
25
Dan Shic1d263b2013-10-04 17:31:38 -070026
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=[],
Nathan Stoddardab583042014-08-11 10:41:47 -070031 extension_paths=[], is_component=True, username=None,
Payton Turnage0b2becc2015-06-17 16:31:14 -070032 password=None, server_port=None, skip_cleanup=False,
33 url_base=None, extra_chromedriver_args=None, *args, **kwargs):
Dan Shic1d263b2013-10-04 17:31:38 -070034 """Initialize.
35
36 @param extra_chrome_flags: Extra chrome flags to pass to chrome, if any.
37 @param subtract_extra_chrome_flags: Remove default flags passed to
38 chrome by chromedriver, if any.
beeps47a51292013-11-20 08:55:23 -080039 @param extension_paths: A list of paths to unzipped extensions. Note
40 that paths to crx files won't work.
41 @param is_component: True if the manifest.json has a key.
Nathan Stoddardab583042014-08-11 10:41:47 -070042 @param username: Log in using this username instead of the default.
Resetswitch98a0f7d2015-05-04 14:48:08 -070043 @param password: Log in using this password instead of the default.
44 @param server_port: Port number for the chromedriver server. If None,
45 an available port is chosen at random.
Resetswitchd1df3d52015-05-20 18:20:55 -070046 @param skip_cleanup: If True, leave the server and browser running
47 so that remote tests can run after this script
48 ends. Default is False.
Payton Turnage0b2becc2015-06-17 16:31:14 -070049 @param url_base: Optional base url for chromedriver.
50 @param extra_chromedriver_args: List of extra arguments to forward to
51 the chromedriver binary, if any.
Dan Shic1d263b2013-10-04 17:31:38 -070052 """
Resetswitchd1df3d52015-05-20 18:20:55 -070053 self._cleanup = not skip_cleanup
Dan Shic1d263b2013-10-04 17:31:38 -070054 assert os.geteuid() == 0, 'Need superuser privileges'
55
56 # Log in with telemetry
beepsbff9f9d2013-12-06 11:14:08 -080057 self._chrome = chrome.Chrome(extension_paths=extension_paths,
58 is_component=is_component,
Nathan Stoddardab583042014-08-11 10:41:47 -070059 username=username,
60 password=password,
beepsbff9f9d2013-12-06 11:14:08 -080061 extra_browser_args=extra_chrome_flags)
62 self._browser = self._chrome.browser
Dan Shi2bff39b2014-03-28 11:36:13 -070063 # Close all tabs owned and opened by Telemetry, as these cannot be
64 # transferred to ChromeDriver.
65 self._browser.tabs[0].Close()
Dan Shic1d263b2013-10-04 17:31:38 -070066
67 # Start ChromeDriver server
Resetswitch98a0f7d2015-05-04 14:48:08 -070068 self._server = chromedriver_server(CHROMEDRIVER_EXE_PATH,
Resetswitchd1df3d52015-05-20 18:20:55 -070069 port=server_port,
Payton Turnage0b2becc2015-06-17 16:31:14 -070070 skip_cleanup=skip_cleanup,
71 url_base=url_base,
72 extra_args=extra_chromedriver_args)
Dan Shic1d263b2013-10-04 17:31:38 -070073
Dan Shi2bff39b2014-03-28 11:36:13 -070074 # Open a new tab using Chrome remote debugging. ChromeDriver expects
75 # a tab opened for remote to work. Tabs opened using Telemetry will be
76 # owned by Telemetry, and will be inaccessible to ChromeDriver.
77 urllib2.urlopen('http://localhost:%i/json/new' %
78 utils.get_chrome_remote_debugging_port())
79
Dan Shic1d263b2013-10-04 17:31:38 -070080 chromeOptions = {'debuggerAddress':
81 ('localhost:%d' %
82 utils.get_chrome_remote_debugging_port())}
83 capabilities = {'chromeOptions':chromeOptions}
84 # Handle to chromedriver, for chrome automation.
Dan Shi96c77cb2013-11-19 10:30:37 -080085 try:
86 self.driver = webdriver.Remote(command_executor=self._server.url,
87 desired_capabilities=capabilities)
88 except NameError:
89 logging.error('selenium module failed to be imported.')
90 raise
Dan Shic1d263b2013-10-04 17:31:38 -070091
92
93 def __enter__(self):
94 return self
95
96
97 def __exit__(self, *args):
98 """Clean up after running the test.
99
100 """
101 if hasattr(self, 'driver') and self.driver:
102 self.driver.close()
103 del self.driver
104
Resetswitchd1df3d52015-05-20 18:20:55 -0700105 if not hasattr(self, '_cleanup') or self._cleanup:
106 if hasattr(self, '_server') and self._server:
107 self._server.close()
108 del self._server
Dan Shic1d263b2013-10-04 17:31:38 -0700109
Resetswitchd1df3d52015-05-20 18:20:55 -0700110 if hasattr(self, '_browser') and self._browser:
111 self._browser.Close()
112 del self._browser
Dan Shic1d263b2013-10-04 17:31:38 -0700113
beepsbff9f9d2013-12-06 11:14:08 -0800114 def get_extension(self, extension_path):
115 """Gets an extension by proxying to the browser.
116
117 @param extension_path: Path to the extension loaded in the browser.
118
119 @return: A telemetry extension object representing the extension.
120 """
121 return self._chrome.get_extension(extension_path)
122
123
Mussaa074a512015-10-20 16:53:23 -0700124 @property
125 def chrome_instance(self):
126 """ The chrome instance used by this chrome driver instance. """
127 return self._chrome
128
129
Dan Shic1d263b2013-10-04 17:31:38 -0700130class chromedriver_server(object):
131 """A running ChromeDriver server.
132
133 This code is migrated from chrome:
134 src/chrome/test/chromedriver/server/server.py
135 """
136
Payton Turnage0b2becc2015-06-17 16:31:14 -0700137 def __init__(self, exe_path, port=None, skip_cleanup=False,
138 url_base=None, extra_args=None):
Dan Shic1d263b2013-10-04 17:31:38 -0700139 """Starts the ChromeDriver server and waits for it to be ready.
140
141 Args:
142 exe_path: path to the ChromeDriver executable
Resetswitch98a0f7d2015-05-04 14:48:08 -0700143 port: server port. If None, an available port is chosen at random.
Resetswitchd1df3d52015-05-20 18:20:55 -0700144 skip_cleanup: If True, leave the server running so that remote
145 tests can run after this script ends. Default is
146 False.
Payton Turnage0b2becc2015-06-17 16:31:14 -0700147 url_base: Optional base url for chromedriver.
148 extra_args: List of extra arguments to forward to the chromedriver
149 binary, if any.
Dan Shic1d263b2013-10-04 17:31:38 -0700150 Raises:
151 RuntimeError if ChromeDriver fails to start
152 """
153 if not os.path.exists(exe_path):
154 raise RuntimeError('ChromeDriver exe not found at: ' + exe_path)
155
Resetswitchd1df3d52015-05-20 18:20:55 -0700156 chromedriver_args = [exe_path]
157 if port:
158 # Allow remote connections if a port was specified
159 chromedriver_args.append('--whitelisted-ips')
160 else:
Resetswitch98a0f7d2015-05-04 14:48:08 -0700161 port = utils.get_unused_port()
Resetswitchd1df3d52015-05-20 18:20:55 -0700162 chromedriver_args.append('--port=%d' % port)
beeps47a51292013-11-20 08:55:23 -0800163
Payton Turnage0b2becc2015-06-17 16:31:14 -0700164 self.url = 'http://localhost:%d' % port
165 if url_base:
166 chromedriver_args.append('--url-base=%s' % url_base)
167 self.url = urlparse.urljoin(self.url, url_base)
168
169 if extra_args:
170 chromedriver_args.extend(extra_args)
171
Ilja H. Friedela6245762015-06-09 15:15:34 -0700172 # TODO(ihf): Remove references to X after M45.
beeps47a51292013-11-20 08:55:23 -0800173 # Chromedriver will look for an X server running on the display
174 # specified through the DISPLAY environment variable.
175 os.environ['DISPLAY'] = X_SERVER_DISPLAY
176 os.environ['XAUTHORITY'] = X_AUTHORITY
177
Dan Shic1d263b2013-10-04 17:31:38 -0700178 self.bg_job = utils.BgJob(chromedriver_args, stderr_level=logging.DEBUG)
Dan Shic1d263b2013-10-04 17:31:38 -0700179 if self.bg_job is None:
180 raise RuntimeError('ChromeDriver server cannot be started')
181
182 try:
183 timeout_msg = 'Timeout on waiting for ChromeDriver to start.'
184 utils.poll_for_condition(self.is_running,
185 exception=utils.TimeoutError(timeout_msg),
186 timeout=10,
187 sleep_interval=.1)
188 except utils.TimeoutError:
189 self.close_bgjob()
190 raise RuntimeError('ChromeDriver server did not start')
191
192 logging.debug('Chrome Driver server is up and listening at port %d.',
193 port)
Resetswitchd1df3d52015-05-20 18:20:55 -0700194 if not skip_cleanup:
195 atexit.register(self.close)
Dan Shic1d263b2013-10-04 17:31:38 -0700196
197
198 def is_running(self):
199 """Returns whether the server is up and running."""
200 try:
201 urllib2.urlopen(self.url + '/status')
202 return True
203 except urllib2.URLError as e:
204 return False
205
206
207 def close_bgjob(self):
208 """Close background job and log stdout and stderr."""
209 utils.nuke_subprocess(self.bg_job.sp)
210 utils.join_bg_jobs([self.bg_job], timeout=1)
211 result = self.bg_job.result
212 if result.stdout or result.stderr:
213 logging.info('stdout of Chrome Driver:\n%s', result.stdout)
214 logging.error('stderr of Chrome Driver:\n%s', result.stderr)
215
216
217 def close(self):
218 """Kills the ChromeDriver server, if it is running."""
219 if self.bg_job is None:
220 return
221
222 try:
223 urllib2.urlopen(self.url + '/shutdown', timeout=10).close()
224 except:
225 pass
226
227 self.close_bgjob()