• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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