• 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        # 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
139class 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