xref: /aosp_15_r20/external/autotest/client/cros/multimedia/facade_resource.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright 2015 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""A module providing common resources for different facades."""
7
8import logging
9import time
10
11from autotest_lib.client.bin import utils
12from autotest_lib.client.common_lib import error
13from autotest_lib.client.common_lib.cros import chrome
14from autotest_lib.client.common_lib.cros import retry
15from autotest_lib.client.cros import constants
16from telemetry.internal.backends.chrome_inspector import devtools_http
17
18import py_utils
19
20_FLAKY_CALL_RETRY_TIMEOUT_SEC = 60
21_FLAKY_CHROME_CALL_RETRY_DELAY_SEC = 1
22
23retry_chrome_call = retry.retry(
24        (chrome.Error, IndexError, Exception),
25        timeout_min=_FLAKY_CALL_RETRY_TIMEOUT_SEC / 60.0,
26        delay_sec=_FLAKY_CHROME_CALL_RETRY_DELAY_SEC)
27
28
29class FacadeResoureError(Exception):
30    """Error in FacadeResource."""
31    pass
32
33
34_FLAKY_CHROME_START_RETRY_TIMEOUT_SEC = 120
35_FLAKY_CHROME_START_RETRY_DELAY_SEC = 10
36
37
38# Telemetry sometimes fails to start Chrome.
39retry_start_chrome = retry.retry(
40        (Exception,),
41        timeout_min=_FLAKY_CHROME_START_RETRY_TIMEOUT_SEC / 60.0,
42        delay_sec=_FLAKY_CHROME_START_RETRY_DELAY_SEC,
43        exception_to_raise=FacadeResoureError,
44        label='Start Chrome')
45
46
47class FacadeResource(object):
48    """This class provides access to telemetry chrome wrapper."""
49
50    ARC_DISABLED = 'disabled'
51    ARC_ENABLED = 'enabled'
52    ARC_VERSION = 'CHROMEOS_ARC_VERSION'
53    EXTRA_BROWSER_ARGS = ['--enable-gpu-benchmarking', '--use-fake-ui-for-media-stream']
54
55    def __init__(self, chrome_object=None, restart=False):
56        """Initializes a FacadeResource.
57
58        @param chrome_object: A chrome.Chrome object or None.
59        @param restart: Preserve the previous browser state.
60
61        """
62        self._chrome = chrome_object
63
64    @property
65    def _browser(self):
66        """Gets the browser object from Chrome."""
67        return self._chrome.browser
68
69
70    @retry_start_chrome
71    def _start_chrome(self, kwargs):
72        """Start a Chrome with given arguments.
73
74        @param kwargs: A dict of keyword arguments passed to Chrome.
75
76        @return: A chrome.Chrome object.
77
78        """
79        logging.debug('Try to start Chrome with kwargs: %s', kwargs)
80        return chrome.Chrome(**kwargs)
81
82
83    def start_custom_chrome(self, kwargs):
84        """Start a custom Chrome with given arguments.
85
86        @param kwargs: A dict of keyword arguments passed to Chrome.
87
88        @return: True on success, False otherwise.
89
90        """
91        # Close the previous Chrome.
92        if self._chrome:
93            self._chrome.close()
94
95        # Start the new Chrome.
96        try:
97            self._chrome = self._start_chrome(kwargs)
98        except FacadeResoureError:
99            logging.error('Failed to start Chrome after retries')
100            return False
101        else:
102            logging.info('Chrome started successfully')
103
104        # The opened tabs are stored by tab descriptors.
105        # Key is the tab descriptor string.
106        # We use string as the key because of RPC Call. Client can use the
107        # string to locate the tab object.
108        # Value is the tab object.
109        self._tabs = dict()
110
111        # Workaround for issue crbug.com/588579.
112        # On daisy, Chrome freezes about 30 seconds after login because of
113        # TPM error. Avoid test accessing Chrome during this time.
114        # Check issue crbug.com/588579 and crbug.com/591646.
115        if utils.get_board() == 'daisy':
116            logging.warning('Delay 30s for issue 588579 on daisy')
117            time.sleep(30)
118
119        return True
120
121
122    def start_default_chrome(self, restart=False, extra_browser_args=None,
123                             disable_arc=False):
124        """Start the default Chrome.
125
126        @param restart: True to start Chrome without clearing previous state.
127        @param extra_browser_args: A list containing extra browser args passed
128                                   to Chrome. This list will be appened to
129                                   default EXTRA_BROWSER_ARGS.
130        @param disable_arc: True to disable ARC++.
131
132        @return: True on success, False otherwise.
133
134        """
135        # TODO: (crbug.com/618111) Add test driven switch for
136        # supporting arc_mode enabled or disabled. At this time
137        # if ARC build is tested, arc_mode is always enabled.
138        if not disable_arc and utils.get_board_property(self.ARC_VERSION):
139            arc_mode = self.ARC_ENABLED
140        else:
141            arc_mode = self.ARC_DISABLED
142        kwargs = {
143            'extension_paths': [constants.AUDIO_TEST_EXTENSION,
144                                constants.DISPLAY_TEST_EXTENSION],
145            'extra_browser_args': self.EXTRA_BROWSER_ARGS,
146            'clear_enterprise_policy': not restart,
147            'arc_mode': arc_mode,
148            'autotest_ext': True
149        }
150        if extra_browser_args:
151            kwargs['extra_browser_args'] += extra_browser_args
152        return self.start_custom_chrome(kwargs)
153
154
155    def __enter__(self):
156        return self
157
158
159    def __exit__(self, *args):
160        if self._chrome:
161            self._chrome.close()
162            self._chrome = None
163
164
165    @staticmethod
166    def _generate_tab_descriptor(tab):
167        """Generate tab descriptor by tab object.
168
169        @param tab: the tab object.
170        @return a str, the tab descriptor of the tab.
171
172        """
173        return hex(id(tab))
174
175
176    def clean_unexpected_tabs(self):
177        """Clean all tabs that are not opened by facade_resource
178
179        It is used to make sure our chrome browser is clean.
180
181        """
182        # If they have the same length we can assume there is no unexpected
183        # tabs.
184        browser_tabs = self.get_tabs()
185        if len(browser_tabs) == len(self._tabs):
186            return
187
188        for tab in browser_tabs:
189            if self._generate_tab_descriptor(tab) not in self._tabs:
190                # TODO(mojahsu): Reevaluate this code. crbug.com/719592
191                try:
192                    tab.Close()
193                except py_utils.TimeoutException:
194                    logging.warning('close tab timeout %r, %s', tab, tab.url)
195
196
197    @retry_chrome_call
198    def get_extension(self, extension_path=None):
199        """Gets the extension from the indicated path.
200
201        @param extension_path: the path of the target extension.
202                               Set to None to get autotest extension.
203                               Defaults to None.
204        @return an extension object.
205
206        @raise RuntimeError if the extension is not found.
207        @raise chrome.Error if the found extension has not yet been
208               retrieved succesfully.
209
210        """
211        try:
212            if extension_path is None:
213                extension = self._chrome.autotest_ext
214            else:
215                extension = self._chrome.get_extension(extension_path)
216        except KeyError as errmsg:
217            # Trigger retry_chrome_call to retry to retrieve the
218            # found extension.
219            raise chrome.Error(errmsg)
220        if not extension:
221            if extension_path is None:
222                raise RuntimeError('Autotest extension not found')
223            else:
224                raise RuntimeError('Extension not found in %r'
225                                    % extension_path)
226        return extension
227
228
229    def get_visible_notifications(self):
230        """Gets the visible notifications
231
232        @return: Returns all visible notifications in list format. Ex:
233                [{title:'', message:'', prority:'', id:''}]
234        """
235        return self._chrome.get_visible_notifications()
236
237
238    @retry_chrome_call
239    def load_url(self, url):
240        """Loads the given url in a new tab. The new tab will be active.
241
242        @param url: The url to load as a string.
243        @return a str, the tab descriptor of the opened tab.
244
245        """
246        tab = self._browser.tabs.New()
247        tab.Navigate(url)
248        tab.Activate()
249        tab.WaitForDocumentReadyStateToBeComplete()
250        tab_descriptor = self._generate_tab_descriptor(tab)
251        self._tabs[tab_descriptor] = tab
252        self.clean_unexpected_tabs()
253        return tab_descriptor
254
255
256    def set_http_server_directories(self, directories):
257        """Starts an HTTP server.
258
259        @param directories: Directories to start serving.
260
261        @return True on success. False otherwise.
262
263        """
264        return self._chrome.browser.platform.SetHTTPServerDirectories(directories)
265
266
267    def http_server_url_of(self, fullpath):
268        """Converts a path to a URL.
269
270        @param fullpath: String containing the full path to the content.
271
272        @return the URL for the provided path.
273
274        """
275        return self._chrome.browser.platform.http_server.UrlOf(fullpath)
276
277
278    def get_tabs(self):
279        """Gets the tabs opened by browser.
280
281        @returns: The tabs attribute in telemetry browser object.
282
283        """
284        return self._browser.tabs
285
286
287    def get_tab_by_descriptor(self, tab_descriptor):
288        """Gets the tab by the tab descriptor.
289
290        @returns: The tab object indicated by the tab descriptor.
291
292        """
293        return self._tabs[tab_descriptor]
294
295
296    @retry_chrome_call
297    def close_tab(self, tab_descriptor):
298        """Closes the tab.
299
300        @param tab_descriptor: Indicate which tab to be closed.
301
302        """
303        if tab_descriptor not in self._tabs:
304            raise RuntimeError('There is no tab for %s' % tab_descriptor)
305        tab = self._tabs[tab_descriptor]
306        del self._tabs[tab_descriptor]
307        tab.Close()
308        self.clean_unexpected_tabs()
309
310
311    def wait_for_javascript_expression(
312            self, tab_descriptor, expression, timeout):
313        """Waits for the given JavaScript expression to be True on the given tab
314
315        @param tab_descriptor: Indicate on which tab to wait for the expression.
316        @param expression: Indiate for what expression to wait.
317        @param timeout: Indicate the timeout of the expression.
318        """
319        if tab_descriptor not in self._tabs:
320            raise RuntimeError('There is no tab for %s' % tab_descriptor)
321        self._tabs[tab_descriptor].WaitForJavaScriptCondition(
322                expression, timeout=timeout)
323
324
325    def execute_javascript(self, tab_descriptor, statement, timeout):
326        """Executes a JavaScript statement on the given tab.
327
328        @param tab_descriptor: Indicate on which tab to execute the statement.
329        @param statement: Indiate what statement to execute.
330        @param timeout: Indicate the timeout of the statement.
331        """
332        if tab_descriptor not in self._tabs:
333            raise RuntimeError('There is no tab for %s' % tab_descriptor)
334        self._tabs[tab_descriptor].ExecuteJavaScript(
335                statement, timeout=timeout)
336
337
338    def evaluate_javascript(self, tab_descriptor, expression, timeout):
339        """Evaluates a JavaScript expression on the given tab.
340
341        @param tab_descriptor: Indicate on which tab to evaluate the expression.
342        @param expression: Indiate what expression to evaluate.
343        @param timeout: Indicate the timeout of the expression.
344        @return the JSONized result of the given expression
345        """
346        if tab_descriptor not in self._tabs:
347            raise RuntimeError('There is no tab for %s' % tab_descriptor)
348        return self._tabs[tab_descriptor].EvaluateJavaScript(
349                expression, timeout=timeout)
350
351class Application(FacadeResource):
352    """ This class provides access to WebStore Applications"""
353
354    APP_NAME_IDS = {
355        'camera' : 'njfbnohfdkmbmnjapinfcopialeghnmh',
356        'files' : 'hhaomjibdihmijegdhdafkllkbggdgoj'
357    }
358    # Time in seconds to load the app
359    LOAD_TIME = 5
360
361    def __init__(self, chrome_object=None):
362        super(Application, self).__init__(chrome_object)
363
364    @retry_chrome_call
365    def evaluate_javascript(self, code):
366        """Executes javascript and returns some result.
367
368        Occasionally calls to EvaluateJavascript on the autotest_ext will fail
369        to find the extension. Instead of wrapping every call in a try/except,
370        calls will go through this function instead.
371
372        @param code: The javascript string to execute
373
374        """
375        try:
376            result = self._chrome.autotest_ext.EvaluateJavaScript(code)
377            return result
378        except KeyError:
379            logging.exception('Could not find autotest_ext')
380        except (devtools_http.DevToolsClientUrlError,
381                devtools_http.DevToolsClientConnectionError):
382            logging.exception('Could not connect to DevTools')
383
384        raise error.TestError("Could not execute %s" % code)
385
386    def click_on(self, ui, name, isRegex=False, role=None):
387        """
388        Click on given role and name matches
389
390        @ui: ui_utils object
391        @param name: item node name.
392        @param isRegex: If name is in regex format then isRegex should be
393                        True otherwise False.
394        @param role: role of the element. Example: button or window etc.
395        @raise error.TestError if the test is failed to find given node
396        """
397        if not ui.item_present(name, isRegex=isRegex, role=role):
398            raise error.TestError("name=%s, role=%s did not appeared with in "
399                                 "time" % (name, role))
400        ui.doDefault_on_obj(name, isRegex=isRegex, role=role)
401
402    def is_app_opened(self, name):
403        """
404        Verify if the Webstore app is opened or not
405
406        @param name: Name of the app to verify.
407
408        """
409        self.evaluate_javascript("var isShown = null;")
410        is_app_shown_js = """
411            chrome.autotestPrivate.isAppShown('%s',
412            function(appShown){isShown = appShown});
413            """ % self.APP_NAME_IDS[name.lower()]
414        self.evaluate_javascript(is_app_shown_js)
415        return self.evaluate_javascript('isShown')
416
417    def launch_app(self, name):
418        """
419        Launch the app/extension by its ID and verify that it opens.
420
421        @param name: Name of the app to launch.
422
423        """
424        logging.info("Launching %s app" % name)
425        if name == "camera":
426            webapps_js = "chrome.autotestPrivate.waitForSystemWebAppsInstall(" \
427                     "function(){})"
428            self.evaluate_javascript(webapps_js)
429            launch_js = "chrome.autotestPrivate.launchSystemWebApp('%s', '%s', " \
430                    "function(){})" % ("Camera",
431                                       "chrome://camera-app/views/main.html")
432        else:
433            launch_js = "chrome.autotestPrivate.launchApp('%s', function(){})" \
434                         % self.APP_NAME_IDS[name.lower()]
435        self.evaluate_javascript(launch_js)
436        def is_app_opened():
437            return self.is_app_opened(name)
438        utils.poll_for_condition(condition=is_app_opened,
439                    desc="%s app is not launched" % name,
440                    timeout=self.LOAD_TIME)
441        logging.info('%s app is launched', name)
442
443    def close_app(self, name):
444        """
445        Close the app/extension by its ID and verify that it closes.
446
447        @param name: Name of the app to close.
448
449        """
450        close_js = "chrome.autotestPrivate.closeApp('%s', function(){})" \
451                    % self.APP_NAME_IDS[name.lower()]
452        self.evaluate_javascript(close_js)
453        def is_app_closed():
454            return not self.is_app_opened(name)
455        utils.poll_for_condition(condition=is_app_closed,
456                    desc="%s app is not closed" % name,
457                    timeout=self.LOAD_TIME)
458        logging.info('%s app is closed', name)
459