xref: /aosp_15_r20/external/autotest/client/common_lib/cros/chromedriver.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Liimport atexit
7*9c5db199SXin Liimport logging
8*9c5db199SXin Liimport os
9*9c5db199SXin Lifrom six.moves import urllib
10*9c5db199SXin Liimport six.moves.urllib.parse
11*9c5db199SXin Li
12*9c5db199SXin Litry:
13*9c5db199SXin Li    from selenium import webdriver
14*9c5db199SXin Liexcept ImportError:
15*9c5db199SXin Li    # Ignore import error, as this can happen when builder tries to call the
16*9c5db199SXin Li    # setup method of test that imports chromedriver.
17*9c5db199SXin Li    logging.error('selenium module failed to be imported.')
18*9c5db199SXin Li    pass
19*9c5db199SXin Li
20*9c5db199SXin Lifrom autotest_lib.client.bin import utils
21*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import chrome
22*9c5db199SXin Li
23*9c5db199SXin LiCHROMEDRIVER_EXE_PATH = '/usr/local/chromedriver/chromedriver'
24*9c5db199SXin LiX_SERVER_DISPLAY = ':0'
25*9c5db199SXin LiX_AUTHORITY = '/home/chronos/.Xauthority'
26*9c5db199SXin Li
27*9c5db199SXin Li
28*9c5db199SXin Liclass chromedriver(object):
29*9c5db199SXin Li    """Wrapper class, a context manager type, for tests to use Chrome Driver."""
30*9c5db199SXin Li
31*9c5db199SXin Li    def __init__(self,
32*9c5db199SXin Li                 extra_chrome_flags=[],
33*9c5db199SXin Li                 subtract_extra_chrome_flags=[],
34*9c5db199SXin Li                 extension_paths=[],
35*9c5db199SXin Li                 username=None,
36*9c5db199SXin Li                 password=None,
37*9c5db199SXin Li                 server_port=None,
38*9c5db199SXin Li                 skip_cleanup=False,
39*9c5db199SXin Li                 url_base=None,
40*9c5db199SXin Li                 extra_chromedriver_args=None,
41*9c5db199SXin Li                 gaia_login=False,
42*9c5db199SXin Li                 disable_default_apps=True,
43*9c5db199SXin Li                 dont_override_profile=False,
44*9c5db199SXin Li                 chromeOptions={},
45*9c5db199SXin Li                 *args,
46*9c5db199SXin Li                 **kwargs):
47*9c5db199SXin Li        """Initialize.
48*9c5db199SXin Li
49*9c5db199SXin Li        @param extra_chrome_flags: Extra chrome flags to pass to chrome, if any.
50*9c5db199SXin Li        @param subtract_extra_chrome_flags: Remove default flags passed to
51*9c5db199SXin Li                chrome by chromedriver, if any.
52*9c5db199SXin Li        @param extension_paths: A list of paths to unzipped extensions. Note
53*9c5db199SXin Li                                that paths to crx files won't work.
54*9c5db199SXin Li        @param username: Log in using this username instead of the default.
55*9c5db199SXin Li        @param password: Log in using this password instead of the default.
56*9c5db199SXin Li        @param server_port: Port number for the chromedriver server. If None,
57*9c5db199SXin Li                            an available port is chosen at random.
58*9c5db199SXin Li        @param skip_cleanup: If True, leave the server and browser running
59*9c5db199SXin Li                             so that remote tests can run after this script
60*9c5db199SXin Li                             ends. Default is False.
61*9c5db199SXin Li        @param url_base: Optional base url for chromedriver.
62*9c5db199SXin Li        @param extra_chromedriver_args: List of extra arguments to forward to
63*9c5db199SXin Li                                        the chromedriver binary, if any.
64*9c5db199SXin Li        @param gaia_login: Logs in to real gaia.
65*9c5db199SXin Li        @param disable_default_apps: For tests that exercise default apps.
66*9c5db199SXin Li        @param dont_override_profile: Don't delete cryptohome before login.
67*9c5db199SXin Li                                      Telemetry will output a warning with this
68*9c5db199SXin Li                                      option.
69*9c5db199SXin Li        """
70*9c5db199SXin Li        if not isinstance(chromeOptions, dict):
71*9c5db199SXin Li            raise TypeError("chromeOptions must be of type dict.")
72*9c5db199SXin Li        self._cleanup = not skip_cleanup
73*9c5db199SXin Li        assert os.geteuid() == 0, 'Need superuser privileges'
74*9c5db199SXin Li
75*9c5db199SXin Li        # When ChromeDriver starts Chrome on other platforms (Linux, Windows,
76*9c5db199SXin Li        # etc.), it accepts flag inputs of the form "--flag_name" or
77*9c5db199SXin Li        # "flag_name". Before starting Chrome with those flags, ChromeDriver
78*9c5db199SXin Li        # reformats them all to "--flag_name". This behavior is copied
79*9c5db199SXin Li        # to ChromeOS for consistency across platforms.
80*9c5db199SXin Li        fixed_extra_chrome_flags = [
81*9c5db199SXin Li            f if f.startswith('--') else '--%s' % f for f in extra_chrome_flags]
82*9c5db199SXin Li
83*9c5db199SXin Li        # Log in with telemetry
84*9c5db199SXin Li        self._chrome = chrome.Chrome(extension_paths=extension_paths,
85*9c5db199SXin Li                                     username=username,
86*9c5db199SXin Li                                     password=password,
87*9c5db199SXin Li                                     extra_browser_args=fixed_extra_chrome_flags,
88*9c5db199SXin Li                                     gaia_login=gaia_login,
89*9c5db199SXin Li                                     disable_default_apps=disable_default_apps,
90*9c5db199SXin Li                                     dont_override_profile=dont_override_profile
91*9c5db199SXin Li                                     )
92*9c5db199SXin Li        self._browser = self._chrome.browser
93*9c5db199SXin Li        # Close all tabs owned and opened by Telemetry, as these cannot be
94*9c5db199SXin Li        # transferred to ChromeDriver.
95*9c5db199SXin Li        self._browser.tabs[0].Close()
96*9c5db199SXin Li
97*9c5db199SXin Li        # Start ChromeDriver server
98*9c5db199SXin Li        self._server = chromedriver_server(CHROMEDRIVER_EXE_PATH,
99*9c5db199SXin Li                                           port=server_port,
100*9c5db199SXin Li                                           skip_cleanup=skip_cleanup,
101*9c5db199SXin Li                                           url_base=url_base,
102*9c5db199SXin Li                                           extra_args=extra_chromedriver_args)
103*9c5db199SXin Li
104*9c5db199SXin Li        # Open a new tab using Chrome remote debugging. ChromeDriver expects
105*9c5db199SXin Li        # a tab opened for remote to work. Tabs opened using Telemetry will be
106*9c5db199SXin Li        # owned by Telemetry, and will be inaccessible to ChromeDriver.
107*9c5db199SXin Li        urllib.request.urlopen('http://localhost:%i/json/new' %
108*9c5db199SXin Li                        utils.get_chrome_remote_debugging_port())
109*9c5db199SXin Li
110*9c5db199SXin Li        chromeBaseOptions = {
111*9c5db199SXin Li                'debuggerAddress':
112*9c5db199SXin Li                ('localhost:%d' % utils.get_chrome_remote_debugging_port())
113*9c5db199SXin Li        }
114*9c5db199SXin Li        chromeOptions.update(chromeBaseOptions)
115*9c5db199SXin Li        capabilities = {'chromeOptions':chromeOptions}
116*9c5db199SXin Li        # Handle to chromedriver, for chrome automation.
117*9c5db199SXin Li        try:
118*9c5db199SXin Li            self.driver = webdriver.Remote(command_executor=self._server.url,
119*9c5db199SXin Li                                           desired_capabilities=capabilities)
120*9c5db199SXin Li        except NameError:
121*9c5db199SXin Li            logging.error('selenium module failed to be imported.')
122*9c5db199SXin Li            raise
123*9c5db199SXin Li
124*9c5db199SXin Li
125*9c5db199SXin Li    def __enter__(self):
126*9c5db199SXin Li        return self
127*9c5db199SXin Li
128*9c5db199SXin Li
129*9c5db199SXin Li    def __exit__(self, *args):
130*9c5db199SXin Li        """Clean up after running the test.
131*9c5db199SXin Li
132*9c5db199SXin Li        """
133*9c5db199SXin Li        if hasattr(self, 'driver') and self.driver:
134*9c5db199SXin Li            self.driver.close()
135*9c5db199SXin Li            del self.driver
136*9c5db199SXin Li
137*9c5db199SXin Li        if not hasattr(self, '_cleanup') or self._cleanup:
138*9c5db199SXin Li            if hasattr(self, '_server') and self._server:
139*9c5db199SXin Li                self._server.close()
140*9c5db199SXin Li                del self._server
141*9c5db199SXin Li
142*9c5db199SXin Li            if hasattr(self, '_browser') and self._browser:
143*9c5db199SXin Li                self._browser.Close()
144*9c5db199SXin Li                del self._browser
145*9c5db199SXin Li
146*9c5db199SXin Li    def get_extension(self, extension_path):
147*9c5db199SXin Li        """Gets an extension by proxying to the browser.
148*9c5db199SXin Li
149*9c5db199SXin Li        @param extension_path: Path to the extension loaded in the browser.
150*9c5db199SXin Li
151*9c5db199SXin Li        @return: A telemetry extension object representing the extension.
152*9c5db199SXin Li        """
153*9c5db199SXin Li        return self._chrome.get_extension(extension_path)
154*9c5db199SXin Li
155*9c5db199SXin Li
156*9c5db199SXin Li    @property
157*9c5db199SXin Li    def chrome_instance(self):
158*9c5db199SXin Li        """ The chrome instance used by this chrome driver instance. """
159*9c5db199SXin Li        return self._chrome
160*9c5db199SXin Li
161*9c5db199SXin Li
162*9c5db199SXin Liclass chromedriver_server(object):
163*9c5db199SXin Li    """A running ChromeDriver server.
164*9c5db199SXin Li
165*9c5db199SXin Li    This code is migrated from chrome:
166*9c5db199SXin Li    src/chrome/test/chromedriver/server/server.py
167*9c5db199SXin Li    """
168*9c5db199SXin Li
169*9c5db199SXin Li    def __init__(self, exe_path, port=None, skip_cleanup=False,
170*9c5db199SXin Li                 url_base=None, extra_args=None):
171*9c5db199SXin Li        """Starts the ChromeDriver server and waits for it to be ready.
172*9c5db199SXin Li
173*9c5db199SXin Li        Args:
174*9c5db199SXin Li            exe_path: path to the ChromeDriver executable
175*9c5db199SXin Li            port: server port. If None, an available port is chosen at random.
176*9c5db199SXin Li            skip_cleanup: If True, leave the server running so that remote
177*9c5db199SXin Li                          tests can run after this script ends. Default is
178*9c5db199SXin Li                          False.
179*9c5db199SXin Li            url_base: Optional base url for chromedriver.
180*9c5db199SXin Li            extra_args: List of extra arguments to forward to the chromedriver
181*9c5db199SXin Li                        binary, if any.
182*9c5db199SXin Li        Raises:
183*9c5db199SXin Li            RuntimeError if ChromeDriver fails to start
184*9c5db199SXin Li        """
185*9c5db199SXin Li        if not os.path.exists(exe_path):
186*9c5db199SXin Li            raise RuntimeError('ChromeDriver exe not found at: ' + exe_path)
187*9c5db199SXin Li
188*9c5db199SXin Li        chromedriver_args = [exe_path]
189*9c5db199SXin Li        if port:
190*9c5db199SXin Li            # Allow remote connections if a port was specified
191*9c5db199SXin Li            chromedriver_args.append('--whitelisted-ips')
192*9c5db199SXin Li        else:
193*9c5db199SXin Li            port = utils.get_unused_port()
194*9c5db199SXin Li        chromedriver_args.append('--port=%d' % port)
195*9c5db199SXin Li
196*9c5db199SXin Li        self.url = 'http://localhost:%d' % port
197*9c5db199SXin Li        if url_base:
198*9c5db199SXin Li            chromedriver_args.append('--url-base=%s' % url_base)
199*9c5db199SXin Li            self.url = six.moves.urllib.parse.urljoin(self.url, url_base)
200*9c5db199SXin Li
201*9c5db199SXin Li        if extra_args:
202*9c5db199SXin Li            chromedriver_args.extend(extra_args)
203*9c5db199SXin Li
204*9c5db199SXin Li        # TODO(ihf): Remove references to X after M45.
205*9c5db199SXin Li        # Chromedriver will look for an X server running on the display
206*9c5db199SXin Li        # specified through the DISPLAY environment variable.
207*9c5db199SXin Li        os.environ['DISPLAY'] = X_SERVER_DISPLAY
208*9c5db199SXin Li        os.environ['XAUTHORITY'] = X_AUTHORITY
209*9c5db199SXin Li
210*9c5db199SXin Li        self.bg_job = utils.BgJob(chromedriver_args, stderr_level=logging.DEBUG)
211*9c5db199SXin Li        if self.bg_job is None:
212*9c5db199SXin Li            raise RuntimeError('ChromeDriver server cannot be started')
213*9c5db199SXin Li
214*9c5db199SXin Li        try:
215*9c5db199SXin Li            timeout_msg = 'Timeout on waiting for ChromeDriver to start.'
216*9c5db199SXin Li            utils.poll_for_condition(self.is_running,
217*9c5db199SXin Li                                     exception=utils.TimeoutError(timeout_msg),
218*9c5db199SXin Li                                     timeout=10,
219*9c5db199SXin Li                                     sleep_interval=.1)
220*9c5db199SXin Li        except utils.TimeoutError:
221*9c5db199SXin Li            self.close_bgjob()
222*9c5db199SXin Li            raise RuntimeError('ChromeDriver server did not start')
223*9c5db199SXin Li
224*9c5db199SXin Li        logging.debug('Chrome Driver server is up and listening at port %d.',
225*9c5db199SXin Li                      port)
226*9c5db199SXin Li        if not skip_cleanup:
227*9c5db199SXin Li            atexit.register(self.close)
228*9c5db199SXin Li
229*9c5db199SXin Li
230*9c5db199SXin Li    def is_running(self):
231*9c5db199SXin Li        """Returns whether the server is up and running."""
232*9c5db199SXin Li        try:
233*9c5db199SXin Li            urllib.request.urlopen(self.url + '/status')
234*9c5db199SXin Li            return True
235*9c5db199SXin Li        except urllib.error.URLError as e:
236*9c5db199SXin Li            return False
237*9c5db199SXin Li
238*9c5db199SXin Li
239*9c5db199SXin Li    def close_bgjob(self):
240*9c5db199SXin Li        """Close background job and log stdout and stderr."""
241*9c5db199SXin Li        utils.nuke_subprocess(self.bg_job.sp)
242*9c5db199SXin Li        utils.join_bg_jobs([self.bg_job], timeout=1)
243*9c5db199SXin Li        result = self.bg_job.result
244*9c5db199SXin Li        if result.stdout or result.stderr:
245*9c5db199SXin Li            logging.info('stdout of Chrome Driver:\n%s', result.stdout)
246*9c5db199SXin Li            logging.error('stderr of Chrome Driver:\n%s', result.stderr)
247*9c5db199SXin Li
248*9c5db199SXin Li
249*9c5db199SXin Li    def close(self):
250*9c5db199SXin Li        """Kills the ChromeDriver server, if it is running."""
251*9c5db199SXin Li        if self.bg_job is None:
252*9c5db199SXin Li            return
253*9c5db199SXin Li
254*9c5db199SXin Li        try:
255*9c5db199SXin Li            urllib.request.urlopen(self.url + '/shutdown', timeout=10).close()
256*9c5db199SXin Li        except:
257*9c5db199SXin Li            pass
258*9c5db199SXin Li
259*9c5db199SXin Li        self.close_bgjob()
260