459 lines
15 KiB
Python
459 lines
15 KiB
Python
|
|
# Lint as: python2, python3
|
||
|
|
# Copyright 2015 The Chromium OS Authors. All rights reserved.
|
||
|
|
# Use of this source code is governed by a BSD-style license that can be
|
||
|
|
# found in the LICENSE file.
|
||
|
|
|
||
|
|
"""A module providing common resources for different facades."""
|
||
|
|
|
||
|
|
import logging
|
||
|
|
import time
|
||
|
|
|
||
|
|
from autotest_lib.client.bin import utils
|
||
|
|
from autotest_lib.client.common_lib import error
|
||
|
|
from autotest_lib.client.common_lib.cros import chrome
|
||
|
|
from autotest_lib.client.common_lib.cros import retry
|
||
|
|
from autotest_lib.client.cros import constants
|
||
|
|
from telemetry.internal.backends.chrome_inspector import devtools_http
|
||
|
|
|
||
|
|
import py_utils
|
||
|
|
|
||
|
|
_FLAKY_CALL_RETRY_TIMEOUT_SEC = 60
|
||
|
|
_FLAKY_CHROME_CALL_RETRY_DELAY_SEC = 1
|
||
|
|
|
||
|
|
retry_chrome_call = retry.retry(
|
||
|
|
(chrome.Error, IndexError, Exception),
|
||
|
|
timeout_min=_FLAKY_CALL_RETRY_TIMEOUT_SEC / 60.0,
|
||
|
|
delay_sec=_FLAKY_CHROME_CALL_RETRY_DELAY_SEC)
|
||
|
|
|
||
|
|
|
||
|
|
class FacadeResoureError(Exception):
|
||
|
|
"""Error in FacadeResource."""
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
_FLAKY_CHROME_START_RETRY_TIMEOUT_SEC = 120
|
||
|
|
_FLAKY_CHROME_START_RETRY_DELAY_SEC = 10
|
||
|
|
|
||
|
|
|
||
|
|
# Telemetry sometimes fails to start Chrome.
|
||
|
|
retry_start_chrome = retry.retry(
|
||
|
|
(Exception,),
|
||
|
|
timeout_min=_FLAKY_CHROME_START_RETRY_TIMEOUT_SEC / 60.0,
|
||
|
|
delay_sec=_FLAKY_CHROME_START_RETRY_DELAY_SEC,
|
||
|
|
exception_to_raise=FacadeResoureError,
|
||
|
|
label='Start Chrome')
|
||
|
|
|
||
|
|
|
||
|
|
class FacadeResource(object):
|
||
|
|
"""This class provides access to telemetry chrome wrapper."""
|
||
|
|
|
||
|
|
ARC_DISABLED = 'disabled'
|
||
|
|
ARC_ENABLED = 'enabled'
|
||
|
|
ARC_VERSION = 'CHROMEOS_ARC_VERSION'
|
||
|
|
EXTRA_BROWSER_ARGS = ['--enable-gpu-benchmarking', '--use-fake-ui-for-media-stream']
|
||
|
|
|
||
|
|
def __init__(self, chrome_object=None, restart=False):
|
||
|
|
"""Initializes a FacadeResource.
|
||
|
|
|
||
|
|
@param chrome_object: A chrome.Chrome object or None.
|
||
|
|
@param restart: Preserve the previous browser state.
|
||
|
|
|
||
|
|
"""
|
||
|
|
self._chrome = chrome_object
|
||
|
|
|
||
|
|
@property
|
||
|
|
def _browser(self):
|
||
|
|
"""Gets the browser object from Chrome."""
|
||
|
|
return self._chrome.browser
|
||
|
|
|
||
|
|
|
||
|
|
@retry_start_chrome
|
||
|
|
def _start_chrome(self, kwargs):
|
||
|
|
"""Start a Chrome with given arguments.
|
||
|
|
|
||
|
|
@param kwargs: A dict of keyword arguments passed to Chrome.
|
||
|
|
|
||
|
|
@return: A chrome.Chrome object.
|
||
|
|
|
||
|
|
"""
|
||
|
|
logging.debug('Try to start Chrome with kwargs: %s', kwargs)
|
||
|
|
return chrome.Chrome(**kwargs)
|
||
|
|
|
||
|
|
|
||
|
|
def start_custom_chrome(self, kwargs):
|
||
|
|
"""Start a custom Chrome with given arguments.
|
||
|
|
|
||
|
|
@param kwargs: A dict of keyword arguments passed to Chrome.
|
||
|
|
|
||
|
|
@return: True on success, False otherwise.
|
||
|
|
|
||
|
|
"""
|
||
|
|
# Close the previous Chrome.
|
||
|
|
if self._chrome:
|
||
|
|
self._chrome.close()
|
||
|
|
|
||
|
|
# Start the new Chrome.
|
||
|
|
try:
|
||
|
|
self._chrome = self._start_chrome(kwargs)
|
||
|
|
except FacadeResoureError:
|
||
|
|
logging.error('Failed to start Chrome after retries')
|
||
|
|
return False
|
||
|
|
else:
|
||
|
|
logging.info('Chrome started successfully')
|
||
|
|
|
||
|
|
# The opened tabs are stored by tab descriptors.
|
||
|
|
# Key is the tab descriptor string.
|
||
|
|
# We use string as the key because of RPC Call. Client can use the
|
||
|
|
# string to locate the tab object.
|
||
|
|
# Value is the tab object.
|
||
|
|
self._tabs = dict()
|
||
|
|
|
||
|
|
# Workaround for issue crbug.com/588579.
|
||
|
|
# On daisy, Chrome freezes about 30 seconds after login because of
|
||
|
|
# TPM error. Avoid test accessing Chrome during this time.
|
||
|
|
# Check issue crbug.com/588579 and crbug.com/591646.
|
||
|
|
if utils.get_board() == 'daisy':
|
||
|
|
logging.warning('Delay 30s for issue 588579 on daisy')
|
||
|
|
time.sleep(30)
|
||
|
|
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
def start_default_chrome(self, restart=False, extra_browser_args=None,
|
||
|
|
disable_arc=False):
|
||
|
|
"""Start the default Chrome.
|
||
|
|
|
||
|
|
@param restart: True to start Chrome without clearing previous state.
|
||
|
|
@param extra_browser_args: A list containing extra browser args passed
|
||
|
|
to Chrome. This list will be appened to
|
||
|
|
default EXTRA_BROWSER_ARGS.
|
||
|
|
@param disable_arc: True to disable ARC++.
|
||
|
|
|
||
|
|
@return: True on success, False otherwise.
|
||
|
|
|
||
|
|
"""
|
||
|
|
# TODO: (crbug.com/618111) Add test driven switch for
|
||
|
|
# supporting arc_mode enabled or disabled. At this time
|
||
|
|
# if ARC build is tested, arc_mode is always enabled.
|
||
|
|
if not disable_arc and utils.get_board_property(self.ARC_VERSION):
|
||
|
|
arc_mode = self.ARC_ENABLED
|
||
|
|
else:
|
||
|
|
arc_mode = self.ARC_DISABLED
|
||
|
|
kwargs = {
|
||
|
|
'extension_paths': [constants.AUDIO_TEST_EXTENSION,
|
||
|
|
constants.DISPLAY_TEST_EXTENSION],
|
||
|
|
'extra_browser_args': self.EXTRA_BROWSER_ARGS,
|
||
|
|
'clear_enterprise_policy': not restart,
|
||
|
|
'arc_mode': arc_mode,
|
||
|
|
'autotest_ext': True
|
||
|
|
}
|
||
|
|
if extra_browser_args:
|
||
|
|
kwargs['extra_browser_args'] += extra_browser_args
|
||
|
|
return self.start_custom_chrome(kwargs)
|
||
|
|
|
||
|
|
|
||
|
|
def __enter__(self):
|
||
|
|
return self
|
||
|
|
|
||
|
|
|
||
|
|
def __exit__(self, *args):
|
||
|
|
if self._chrome:
|
||
|
|
self._chrome.close()
|
||
|
|
self._chrome = None
|
||
|
|
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _generate_tab_descriptor(tab):
|
||
|
|
"""Generate tab descriptor by tab object.
|
||
|
|
|
||
|
|
@param tab: the tab object.
|
||
|
|
@return a str, the tab descriptor of the tab.
|
||
|
|
|
||
|
|
"""
|
||
|
|
return hex(id(tab))
|
||
|
|
|
||
|
|
|
||
|
|
def clean_unexpected_tabs(self):
|
||
|
|
"""Clean all tabs that are not opened by facade_resource
|
||
|
|
|
||
|
|
It is used to make sure our chrome browser is clean.
|
||
|
|
|
||
|
|
"""
|
||
|
|
# If they have the same length we can assume there is no unexpected
|
||
|
|
# tabs.
|
||
|
|
browser_tabs = self.get_tabs()
|
||
|
|
if len(browser_tabs) == len(self._tabs):
|
||
|
|
return
|
||
|
|
|
||
|
|
for tab in browser_tabs:
|
||
|
|
if self._generate_tab_descriptor(tab) not in self._tabs:
|
||
|
|
# TODO(mojahsu): Reevaluate this code. crbug.com/719592
|
||
|
|
try:
|
||
|
|
tab.Close()
|
||
|
|
except py_utils.TimeoutException:
|
||
|
|
logging.warning('close tab timeout %r, %s', tab, tab.url)
|
||
|
|
|
||
|
|
|
||
|
|
@retry_chrome_call
|
||
|
|
def get_extension(self, extension_path=None):
|
||
|
|
"""Gets the extension from the indicated path.
|
||
|
|
|
||
|
|
@param extension_path: the path of the target extension.
|
||
|
|
Set to None to get autotest extension.
|
||
|
|
Defaults to None.
|
||
|
|
@return an extension object.
|
||
|
|
|
||
|
|
@raise RuntimeError if the extension is not found.
|
||
|
|
@raise chrome.Error if the found extension has not yet been
|
||
|
|
retrieved succesfully.
|
||
|
|
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
if extension_path is None:
|
||
|
|
extension = self._chrome.autotest_ext
|
||
|
|
else:
|
||
|
|
extension = self._chrome.get_extension(extension_path)
|
||
|
|
except KeyError as errmsg:
|
||
|
|
# Trigger retry_chrome_call to retry to retrieve the
|
||
|
|
# found extension.
|
||
|
|
raise chrome.Error(errmsg)
|
||
|
|
if not extension:
|
||
|
|
if extension_path is None:
|
||
|
|
raise RuntimeError('Autotest extension not found')
|
||
|
|
else:
|
||
|
|
raise RuntimeError('Extension not found in %r'
|
||
|
|
% extension_path)
|
||
|
|
return extension
|
||
|
|
|
||
|
|
|
||
|
|
def get_visible_notifications(self):
|
||
|
|
"""Gets the visible notifications
|
||
|
|
|
||
|
|
@return: Returns all visible notifications in list format. Ex:
|
||
|
|
[{title:'', message:'', prority:'', id:''}]
|
||
|
|
"""
|
||
|
|
return self._chrome.get_visible_notifications()
|
||
|
|
|
||
|
|
|
||
|
|
@retry_chrome_call
|
||
|
|
def load_url(self, url):
|
||
|
|
"""Loads the given url in a new tab. The new tab will be active.
|
||
|
|
|
||
|
|
@param url: The url to load as a string.
|
||
|
|
@return a str, the tab descriptor of the opened tab.
|
||
|
|
|
||
|
|
"""
|
||
|
|
tab = self._browser.tabs.New()
|
||
|
|
tab.Navigate(url)
|
||
|
|
tab.Activate()
|
||
|
|
tab.WaitForDocumentReadyStateToBeComplete()
|
||
|
|
tab_descriptor = self._generate_tab_descriptor(tab)
|
||
|
|
self._tabs[tab_descriptor] = tab
|
||
|
|
self.clean_unexpected_tabs()
|
||
|
|
return tab_descriptor
|
||
|
|
|
||
|
|
|
||
|
|
def set_http_server_directories(self, directories):
|
||
|
|
"""Starts an HTTP server.
|
||
|
|
|
||
|
|
@param directories: Directories to start serving.
|
||
|
|
|
||
|
|
@return True on success. False otherwise.
|
||
|
|
|
||
|
|
"""
|
||
|
|
return self._chrome.browser.platform.SetHTTPServerDirectories(directories)
|
||
|
|
|
||
|
|
|
||
|
|
def http_server_url_of(self, fullpath):
|
||
|
|
"""Converts a path to a URL.
|
||
|
|
|
||
|
|
@param fullpath: String containing the full path to the content.
|
||
|
|
|
||
|
|
@return the URL for the provided path.
|
||
|
|
|
||
|
|
"""
|
||
|
|
return self._chrome.browser.platform.http_server.UrlOf(fullpath)
|
||
|
|
|
||
|
|
|
||
|
|
def get_tabs(self):
|
||
|
|
"""Gets the tabs opened by browser.
|
||
|
|
|
||
|
|
@returns: The tabs attribute in telemetry browser object.
|
||
|
|
|
||
|
|
"""
|
||
|
|
return self._browser.tabs
|
||
|
|
|
||
|
|
|
||
|
|
def get_tab_by_descriptor(self, tab_descriptor):
|
||
|
|
"""Gets the tab by the tab descriptor.
|
||
|
|
|
||
|
|
@returns: The tab object indicated by the tab descriptor.
|
||
|
|
|
||
|
|
"""
|
||
|
|
return self._tabs[tab_descriptor]
|
||
|
|
|
||
|
|
|
||
|
|
@retry_chrome_call
|
||
|
|
def close_tab(self, tab_descriptor):
|
||
|
|
"""Closes the tab.
|
||
|
|
|
||
|
|
@param tab_descriptor: Indicate which tab to be closed.
|
||
|
|
|
||
|
|
"""
|
||
|
|
if tab_descriptor not in self._tabs:
|
||
|
|
raise RuntimeError('There is no tab for %s' % tab_descriptor)
|
||
|
|
tab = self._tabs[tab_descriptor]
|
||
|
|
del self._tabs[tab_descriptor]
|
||
|
|
tab.Close()
|
||
|
|
self.clean_unexpected_tabs()
|
||
|
|
|
||
|
|
|
||
|
|
def wait_for_javascript_expression(
|
||
|
|
self, tab_descriptor, expression, timeout):
|
||
|
|
"""Waits for the given JavaScript expression to be True on the given tab
|
||
|
|
|
||
|
|
@param tab_descriptor: Indicate on which tab to wait for the expression.
|
||
|
|
@param expression: Indiate for what expression to wait.
|
||
|
|
@param timeout: Indicate the timeout of the expression.
|
||
|
|
"""
|
||
|
|
if tab_descriptor not in self._tabs:
|
||
|
|
raise RuntimeError('There is no tab for %s' % tab_descriptor)
|
||
|
|
self._tabs[tab_descriptor].WaitForJavaScriptCondition(
|
||
|
|
expression, timeout=timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def execute_javascript(self, tab_descriptor, statement, timeout):
|
||
|
|
"""Executes a JavaScript statement on the given tab.
|
||
|
|
|
||
|
|
@param tab_descriptor: Indicate on which tab to execute the statement.
|
||
|
|
@param statement: Indiate what statement to execute.
|
||
|
|
@param timeout: Indicate the timeout of the statement.
|
||
|
|
"""
|
||
|
|
if tab_descriptor not in self._tabs:
|
||
|
|
raise RuntimeError('There is no tab for %s' % tab_descriptor)
|
||
|
|
self._tabs[tab_descriptor].ExecuteJavaScript(
|
||
|
|
statement, timeout=timeout)
|
||
|
|
|
||
|
|
|
||
|
|
def evaluate_javascript(self, tab_descriptor, expression, timeout):
|
||
|
|
"""Evaluates a JavaScript expression on the given tab.
|
||
|
|
|
||
|
|
@param tab_descriptor: Indicate on which tab to evaluate the expression.
|
||
|
|
@param expression: Indiate what expression to evaluate.
|
||
|
|
@param timeout: Indicate the timeout of the expression.
|
||
|
|
@return the JSONized result of the given expression
|
||
|
|
"""
|
||
|
|
if tab_descriptor not in self._tabs:
|
||
|
|
raise RuntimeError('There is no tab for %s' % tab_descriptor)
|
||
|
|
return self._tabs[tab_descriptor].EvaluateJavaScript(
|
||
|
|
expression, timeout=timeout)
|
||
|
|
|
||
|
|
class Application(FacadeResource):
|
||
|
|
""" This class provides access to WebStore Applications"""
|
||
|
|
|
||
|
|
APP_NAME_IDS = {
|
||
|
|
'camera' : 'njfbnohfdkmbmnjapinfcopialeghnmh',
|
||
|
|
'files' : 'hhaomjibdihmijegdhdafkllkbggdgoj'
|
||
|
|
}
|
||
|
|
# Time in seconds to load the app
|
||
|
|
LOAD_TIME = 5
|
||
|
|
|
||
|
|
def __init__(self, chrome_object=None):
|
||
|
|
super(Application, self).__init__(chrome_object)
|
||
|
|
|
||
|
|
@retry_chrome_call
|
||
|
|
def evaluate_javascript(self, code):
|
||
|
|
"""Executes javascript and returns some result.
|
||
|
|
|
||
|
|
Occasionally calls to EvaluateJavascript on the autotest_ext will fail
|
||
|
|
to find the extension. Instead of wrapping every call in a try/except,
|
||
|
|
calls will go through this function instead.
|
||
|
|
|
||
|
|
@param code: The javascript string to execute
|
||
|
|
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
result = self._chrome.autotest_ext.EvaluateJavaScript(code)
|
||
|
|
return result
|
||
|
|
except KeyError:
|
||
|
|
logging.exception('Could not find autotest_ext')
|
||
|
|
except (devtools_http.DevToolsClientUrlError,
|
||
|
|
devtools_http.DevToolsClientConnectionError):
|
||
|
|
logging.exception('Could not connect to DevTools')
|
||
|
|
|
||
|
|
raise error.TestError("Could not execute %s" % code)
|
||
|
|
|
||
|
|
def click_on(self, ui, name, isRegex=False, role=None):
|
||
|
|
"""
|
||
|
|
Click on given role and name matches
|
||
|
|
|
||
|
|
@ui: ui_utils object
|
||
|
|
@param name: item node name.
|
||
|
|
@param isRegex: If name is in regex format then isRegex should be
|
||
|
|
True otherwise False.
|
||
|
|
@param role: role of the element. Example: button or window etc.
|
||
|
|
@raise error.TestError if the test is failed to find given node
|
||
|
|
"""
|
||
|
|
if not ui.item_present(name, isRegex=isRegex, role=role):
|
||
|
|
raise error.TestError("name=%s, role=%s did not appeared with in "
|
||
|
|
"time" % (name, role))
|
||
|
|
ui.doDefault_on_obj(name, isRegex=isRegex, role=role)
|
||
|
|
|
||
|
|
def is_app_opened(self, name):
|
||
|
|
"""
|
||
|
|
Verify if the Webstore app is opened or not
|
||
|
|
|
||
|
|
@param name: Name of the app to verify.
|
||
|
|
|
||
|
|
"""
|
||
|
|
self.evaluate_javascript("var isShown = null;")
|
||
|
|
is_app_shown_js = """
|
||
|
|
chrome.autotestPrivate.isAppShown('%s',
|
||
|
|
function(appShown){isShown = appShown});
|
||
|
|
""" % self.APP_NAME_IDS[name.lower()]
|
||
|
|
self.evaluate_javascript(is_app_shown_js)
|
||
|
|
return self.evaluate_javascript('isShown')
|
||
|
|
|
||
|
|
def launch_app(self, name):
|
||
|
|
"""
|
||
|
|
Launch the app/extension by its ID and verify that it opens.
|
||
|
|
|
||
|
|
@param name: Name of the app to launch.
|
||
|
|
|
||
|
|
"""
|
||
|
|
logging.info("Launching %s app" % name)
|
||
|
|
if name == "camera":
|
||
|
|
webapps_js = "chrome.autotestPrivate.waitForSystemWebAppsInstall(" \
|
||
|
|
"function(){})"
|
||
|
|
self.evaluate_javascript(webapps_js)
|
||
|
|
launch_js = "chrome.autotestPrivate.launchSystemWebApp('%s', '%s', " \
|
||
|
|
"function(){})" % ("Camera",
|
||
|
|
"chrome://camera-app/views/main.html")
|
||
|
|
else:
|
||
|
|
launch_js = "chrome.autotestPrivate.launchApp('%s', function(){})" \
|
||
|
|
% self.APP_NAME_IDS[name.lower()]
|
||
|
|
self.evaluate_javascript(launch_js)
|
||
|
|
def is_app_opened():
|
||
|
|
return self.is_app_opened(name)
|
||
|
|
utils.poll_for_condition(condition=is_app_opened,
|
||
|
|
desc="%s app is not launched" % name,
|
||
|
|
timeout=self.LOAD_TIME)
|
||
|
|
logging.info('%s app is launched', name)
|
||
|
|
|
||
|
|
def close_app(self, name):
|
||
|
|
"""
|
||
|
|
Close the app/extension by its ID and verify that it closes.
|
||
|
|
|
||
|
|
@param name: Name of the app to close.
|
||
|
|
|
||
|
|
"""
|
||
|
|
close_js = "chrome.autotestPrivate.closeApp('%s', function(){})" \
|
||
|
|
% self.APP_NAME_IDS[name.lower()]
|
||
|
|
self.evaluate_javascript(close_js)
|
||
|
|
def is_app_closed():
|
||
|
|
return not self.is_app_opened(name)
|
||
|
|
utils.poll_for_condition(condition=is_app_closed,
|
||
|
|
desc="%s app is not closed" % name,
|
||
|
|
timeout=self.LOAD_TIME)
|
||
|
|
logging.info('%s app is closed', name)
|