Home | History | Annotate | Download | only in cros
      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         # 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 
    147 class 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