1# Copyright 2015 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"""A module providing common resources for different facades."""
6
7import exceptions
8import logging
9import time
10
11from autotest_lib.client.bin import utils
12from autotest_lib.client.common_lib.cros import chrome
13from autotest_lib.client.common_lib.cros import retry
14from autotest_lib.client.cros import constants
15
16_FLAKY_CALL_RETRY_TIMEOUT_SEC = 60
17_FLAKY_CHROME_CALL_RETRY_DELAY_SEC = 1
18
19retry_chrome_call = retry.retry(
20        (chrome.Error, exceptions.IndexError, exceptions.Exception),
21        timeout_min=_FLAKY_CALL_RETRY_TIMEOUT_SEC / 60.0,
22        delay_sec=_FLAKY_CHROME_CALL_RETRY_DELAY_SEC)
23
24
25class FacadeResoureError(Exception):
26    """Error in FacadeResource."""
27    pass
28
29
30_FLAKY_CHROME_START_RETRY_TIMEOUT_SEC = 120
31_FLAKY_CHROME_START_RETRY_DELAY_SEC = 10
32
33
34# Telemetry sometimes fails to start Chrome.
35retry_start_chrome = retry.retry(
36        (Exception,),
37        timeout_min=_FLAKY_CHROME_START_RETRY_TIMEOUT_SEC / 60.0,
38        delay_sec=_FLAKY_CHROME_START_RETRY_DELAY_SEC,
39        exception_to_raise=FacadeResoureError,
40        label='Start Chrome')
41
42
43class FacadeResource(object):
44    """This class provides access to telemetry chrome wrapper."""
45
46    ARC_DISABLED = 'disabled'
47    ARC_ENABLED = 'enabled'
48    ARC_VERSION = 'CHROMEOS_ARC_VERSION'
49    EXTRA_BROWSER_ARGS = ['--enable-gpu-benchmarking', '--use-fake-ui-for-media-stream']
50
51    def __init__(self, chrome_object=None, restart=False):
52        """Initializes a FacadeResource.
53
54        @param chrome_object: A chrome.Chrome object or None.
55        @param restart: Preserve the previous browser state.
56
57        """
58        self._chrome = chrome_object
59
60    @property
61    def _browser(self):
62        """Gets the browser object from Chrome."""
63        return self._chrome.browser
64
65
66    @retry_start_chrome
67    def _start_chrome(self, kwargs):
68        """Start a Chrome with given arguments.
69
70        @param kwargs: A dict of keyword arguments passed to Chrome.
71
72        @return: A chrome.Chrome object.
73
74        """
75        logging.debug('Try to start Chrome with kwargs: %s', kwargs)
76        return chrome.Chrome(**kwargs)
77
78
79    def start_custom_chrome(self, kwargs):
80        """Start a custom Chrome with given arguments.
81
82        @param kwargs: A dict of keyword arguments passed to Chrome.
83
84        @return: True on success, False otherwise.
85
86        """
87        # Close the previous Chrome.
88        if self._chrome:
89            self._chrome.close()
90
91        # Start the new Chrome.
92        try:
93            self._chrome = self._start_chrome(kwargs)
94        except FacadeResoureError:
95            logging.error('Failed to start Chrome after retries')
96            return False
97        else:
98            logging.info('Chrome started successfully')
99
100        # The opened tabs are stored by tab descriptors.
101        # Key is the tab descriptor string.
102        # We use string as the key because of RPC Call. Client can use the
103        # string to locate the tab object.
104        # Value is the tab object.
105        self._tabs = dict()
106
107        # Workaround for issue crbug.com/588579.
108        # On daisy, Chrome freezes about 30 seconds after login because of
109        # TPM error. Avoid test accessing Chrome during this time.
110        # Check issue crbug.com/588579 and crbug.com/591646.
111        if utils.get_board() == 'daisy':
112            logging.warning('Delay 30s for issue 588579 on daisy')
113            time.sleep(30)
114
115        return True
116
117
118    def start_default_chrome(self, restart=False, extra_browser_args=None):
119        """Start the default Chrome.
120
121        @param restart: True to start Chrome without clearing previous state.
122        @param extra_browser_args: A list containing extra browser args passed
123                                   to Chrome. This list will be appened to
124                                   default EXTRA_BROWSER_ARGS.
125
126        @return: True on success, False otherwise.
127
128        """
129        # TODO: (crbug.com/618111) Add test driven switch for
130        # supporting arc_mode enabled or disabled. At this time
131        # if ARC build is tested, arc_mode is always enabled.
132        arc_mode = self.ARC_DISABLED
133        if utils.get_board_property(self.ARC_VERSION):
134            arc_mode = self.ARC_ENABLED
135        kwargs = {
136            'extension_paths': [constants.MULTIMEDIA_TEST_EXTENSION],
137            'extra_browser_args': self.EXTRA_BROWSER_ARGS,
138            'clear_enterprise_policy': not restart,
139            'arc_mode': arc_mode,
140            'autotest_ext': True
141        }
142        if extra_browser_args:
143            kwargs['extra_browser_args'] += extra_browser_args
144        return self.start_custom_chrome(kwargs)
145
146
147    def __enter__(self):
148        return self
149
150
151    def __exit__(self, *args):
152        if self._chrome:
153            self._chrome.close()
154            self._chrome = None
155
156
157    @staticmethod
158    def _generate_tab_descriptor(tab):
159        """Generate tab descriptor by tab object.
160
161        @param tab: the tab object.
162        @return a str, the tab descriptor of the tab.
163
164        """
165        return hex(id(tab))
166
167
168    def clean_unexpected_tabs(self):
169        """Clean all tabs that are not opened by facade_resource
170
171        It is used to make sure our chrome browser is clean.
172
173        """
174        # If they have the same length we can assume there is no unexpected
175        # tabs.
176        browser_tabs = self.get_tabs()
177        if len(browser_tabs) == len(self._tabs):
178            return
179
180        for tab in browser_tabs:
181            if self._generate_tab_descriptor(tab) not in self._tabs:
182                tab.Close()
183
184
185    @retry_chrome_call
186    def get_extension(self, extension_path=None):
187        """Gets the extension from the indicated path.
188
189        @param extension_path: the path of the target extension.
190                               Set to None to get autotest extension.
191                               Defaults to None.
192        @return an extension object.
193
194        @raise RuntimeError if the extension is not found.
195        @raise chrome.Error if the found extension has not yet been
196               retrieved succesfully.
197
198        """
199        try:
200            if extension_path is None:
201                extension = self._chrome.autotest_ext
202            else:
203                extension = self._chrome.get_extension(extension_path)
204        except KeyError, errmsg:
205            # Trigger retry_chrome_call to retry to retrieve the
206            # found extension.
207            raise chrome.Error(errmsg)
208        if not extension:
209            if extension_path is None:
210                raise RuntimeError('Autotest extension not found')
211            else:
212                raise RuntimeError('Extension not found in %r'
213                                    % extension_path)
214        return extension
215
216
217    @retry_chrome_call
218    def load_url(self, url):
219        """Loads the given url in a new tab. The new tab will be active.
220
221        @param url: The url to load as a string.
222        @return a str, the tab descriptor of the opened tab.
223
224        """
225        tab = self._browser.tabs.New()
226        tab.Navigate(url)
227        tab.Activate()
228        tab.WaitForDocumentReadyStateToBeComplete()
229        tab_descriptor = self._generate_tab_descriptor(tab)
230        self._tabs[tab_descriptor] = tab
231        self.clean_unexpected_tabs()
232        return tab_descriptor
233
234
235    def get_tabs(self):
236        """Gets the tabs opened by browser.
237
238        @returns: The tabs attribute in telemetry browser object.
239
240        """
241        return self._browser.tabs
242
243
244    def get_tab_by_descriptor(self, tab_descriptor):
245        """Gets the tab by the tab descriptor.
246
247        @returns: The tab object indicated by the tab descriptor.
248
249        """
250        return self._tabs[tab_descriptor]
251
252
253    @retry_chrome_call
254    def close_tab(self, tab_descriptor):
255        """Closes the tab.
256
257        @param tab_descriptor: Indicate which tab to be closed.
258
259        """
260        if tab_descriptor not in self._tabs:
261            raise RuntimeError('There is no tab for %s' % tab_descriptor)
262        tab = self._tabs[tab_descriptor]
263        del self._tabs[tab_descriptor]
264        tab.Close()
265        self.clean_unexpected_tabs()
266
267
268    def wait_for_javascript_expression(
269            self, tab_descriptor, expression, timeout):
270        """Waits for the given JavaScript expression to be True on the given tab
271
272        @param tab_descriptor: Indicate on which tab to wait for the expression.
273        @param expression: Indiate for what expression to wait.
274        @param timeout: Indicate the timeout of the expression.
275        """
276        if tab_descriptor not in self._tabs:
277            raise RuntimeError('There is no tab for %s' % tab_descriptor)
278        self._tabs[tab_descriptor].WaitForJavaScriptCondition(
279                expression, timeout=timeout)
280
281
282    def execute_javascript(self, tab_descriptor, statement, timeout):
283        """Executes a JavaScript statement on the given tab.
284
285        @param tab_descriptor: Indicate on which tab to execute the statement.
286        @param statement: Indiate what statement to execute.
287        @param timeout: Indicate the timeout of the statement.
288        """
289        if tab_descriptor not in self._tabs:
290            raise RuntimeError('There is no tab for %s' % tab_descriptor)
291        self._tabs[tab_descriptor].ExecuteJavaScript(
292                statement, timeout=timeout)
293
294
295    def evaluate_javascript(self, tab_descriptor, expression, timeout):
296        """Evaluates a JavaScript expression on the given tab.
297
298        @param tab_descriptor: Indicate on which tab to evaluate the expression.
299        @param expression: Indiate what expression to evaluate.
300        @param timeout: Indicate the timeout of the expression.
301        @return the JSONized result of the given expression
302        """
303        if tab_descriptor not in self._tabs:
304            raise RuntimeError('There is no tab for %s' % tab_descriptor)
305        return self._tabs[tab_descriptor].EvaluateJavaScript(
306                expression, timeout=timeout)
307